diff --git a/.env.example b/.env.example index c610659..0f7aa32 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,7 @@ +# support to 64bit integers: number, bigint or mixed +SAFE_INTEGER_MODE=number + # chinook database used in non-destructive tests CHINOOK_DATABASE_URL="sqlitecloud://user:password@xxx.sqlite.cloud:8860/chinook.sqlite" diff --git a/package-lock.json b/package-lock.json index b1e7e3d..039d253 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,38 +1,38 @@ { "name": "@sqlitecloud/drivers", - "version": "1.0.438", + "version": "1.0.507", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@sqlitecloud/drivers", - "version": "1.0.438", + "version": "1.0.507", "license": "MIT", "dependencies": { "buffer": "^6.0.3", "eventemitter3": "^5.0.1", "lz4js": "^0.2.0", "socket.io-client": "^4.8.1", - "whatwg-url": "^14.1.1" + "whatwg-url": "^14.2.0" }, "devDependencies": { "@types/jest": "^29.5.14", "@types/lz4": "^0.6.4", - "@types/node": "^22.13.8", + "@types/node": "^22.14.1", "@types/whatwg-url": "^13.0.0", - "dotenv": "^16.4.7", + "dotenv": "^16.5.0", "dotenv-cli": "^8.0.0", "husky": "^9.1.7", "jest": "^29.7.0", - "jest-html-reporter": "^4.0.1", + "jest-html-reporter": "^4.1.0", "prettier": "^3.5.3", "sqlite3": "^5.1.7", - "ts-jest": "^29.2.6", + "ts-jest": "^29.3.2", "ts-node": "^10.9.2", - "typedoc": "^0.27.9", - "typedoc-plugin-markdown": "^4.4.2", - "typescript": "^5.8.2", - "webpack": "^5.98.0", + "typedoc": "^0.28.2", + "typedoc-plugin-markdown": "^4.6.2", + "typescript": "^5.8.3", + "webpack": "^5.99.5", "webpack-cli": "^6.0.1" }, "engines": { @@ -6810,15 +6810,17 @@ "optional": true }, "node_modules/@gerrit0/mini-shiki": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-1.24.1.tgz", - "integrity": "sha512-PNP/Gjv3VqU7z7DjRgO3F9Ok5frTKqtpV+LJW1RzMcr2zpRk0ulhEWnbcNGXzPC7BZyWMIHrkfQX2GZRfxrn6Q==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.4.2.tgz", + "integrity": "sha512-3jXo5bNjvvimvdbIhKGfFxSnKCX+MA8wzHv55ptzk/cx8wOzT+BRcYgj8aFN3yTiTs+zvQQiaZFr7Jce1ZG3fw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/engine-oniguruma": "^1.24.0", - "@shikijs/types": "^1.24.0", - "@shikijs/vscode-textmate": "^9.3.0" + "@shikijs/engine-oniguruma": "^3.4.2", + "@shikijs/langs": "^3.4.2", + "@shikijs/themes": "^3.4.2", + "@shikijs/types": "^3.4.2", + "@shikijs/vscode-textmate": "^10.0.2" } }, "node_modules/@hapi/hoek": { @@ -8117,31 +8119,51 @@ } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.0.tgz", - "integrity": "sha512-Eua0qNOL73Y82lGA4GF5P+G2+VXX9XnuUxkiUuwcxQPH4wom+tE39kZpBFXfUuwNYxHSkrSxpB1p4kyRW0moSg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.4.2.tgz", + "integrity": "sha512-zcZKMnNndgRa3ORja6Iemsr3DrLtkX3cAF7lTJkdMB6v9alhlBsX9uNiCpqofNrXOvpA3h6lHcLJxgCIhVOU5Q==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "1.24.0", - "@shikijs/vscode-textmate": "^9.3.0" + "@shikijs/types": "3.4.2", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.4.2.tgz", + "integrity": "sha512-H6azIAM+OXD98yztIfs/KH5H4PU39t+SREhmM8LaNXyUrqj2mx+zVkr8MWYqjceSjDw9I1jawm1WdFqU806rMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.4.2" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.4.2.tgz", + "integrity": "sha512-qAEuAQh+brd8Jyej2UDDf+b4V2g1Rm8aBIdvt32XhDPrHvDkEnpb7Kzc9hSuHUxz0Iuflmq7elaDuQAP9bHIhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.4.2" } }, "node_modules/@shikijs/types": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.24.0.tgz", - "integrity": "sha512-aptbEuq1Pk88DMlCe+FzXNnBZ17LCiLIGWAeCWhoFDzia5Q5Krx3DgnULLiouSdd6+LUM39XwXGppqYE0Ghtug==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.4.2.tgz", + "integrity": "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^9.3.0", + "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "node_modules/@shikijs/vscode-textmate": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz", - "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "dev": true, "license": "MIT" }, @@ -8345,12 +8367,12 @@ } }, "node_modules/@types/node": { - "version": "22.13.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz", - "integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==", + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/node-forge": { @@ -8361,12 +8383,6 @@ "@types/node": "*" } }, - "node_modules/@types/node/node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "license": "MIT" - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "license": "MIT" @@ -9949,9 +9965,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -11583,9 +11599,9 @@ } }, "node_modules/jest-html-reporter": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jest-html-reporter/-/jest-html-reporter-4.0.1.tgz", - "integrity": "sha512-AvE1/z59BbkNFC/GQa8eL7x4V8uN1vjW2QKF8+OqG89dppCnF3jgEU3Ix5ia8SA0c9or8YFhsILAjGtgi3I/mg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jest-html-reporter/-/jest-html-reporter-4.1.0.tgz", + "integrity": "sha512-hB/vBlNBAbH0/L9UiV29uWNhyk+YrSMnu/0vKuhZGrdiiZgpfKBoyojJjskIJbOWCWh2t/3/QyRU2nrFvhqGpw==", "dev": true, "license": "MIT", "dependencies": { @@ -14557,9 +14573,9 @@ } }, "node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14589,9 +14605,9 @@ } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -15374,7 +15390,9 @@ } }, "node_modules/tr46": { - "version": "5.0.0", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -15384,9 +15402,9 @@ } }, "node_modules/ts-jest": { - "version": "29.2.6", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz", - "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==", + "version": "29.3.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz", + "integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==", "dev": true, "license": "MIT", "dependencies": { @@ -15397,7 +15415,8 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.1", + "semver": "^7.7.2", + "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "bin": { @@ -15497,40 +15516,54 @@ "node": ">=4" } }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typedoc": { - "version": "0.27.9", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.9.tgz", - "integrity": "sha512-/z585740YHURLl9DN2jCWe6OW7zKYm6VoQ93H0sxZ1cwHQEQrUn5BJrEnkWhfzUdyO+BLGjnKUZ9iz9hKloFDw==", + "version": "0.28.4", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.4.tgz", + "integrity": "sha512-xKvKpIywE1rnqqLgjkoq0F3wOqYaKO9nV6YkkSat6IxOWacUCc/7Es0hR3OPmkIqkPoEn7U3x+sYdG72rstZQA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@gerrit0/mini-shiki": "^1.24.0", + "@gerrit0/mini-shiki": "^3.2.2", "lunr": "^2.3.9", "markdown-it": "^14.1.0", "minimatch": "^9.0.5", - "yaml": "^2.6.1" + "yaml": "^2.7.1" }, "bin": { "typedoc": "bin/typedoc" }, "engines": { - "node": ">= 18" + "node": ">= 18", + "pnpm": ">= 10" }, "peerDependencies": { "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x" } }, "node_modules/typedoc-plugin-markdown": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.4.2.tgz", - "integrity": "sha512-kJVkU2Wd+AXQpyL6DlYXXRrfNrHrEIUgiABWH8Z+2Lz5Sq6an4dQ/hfvP75bbokjNDUskOdFlEEm/0fSVyC7eg==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.6.3.tgz", + "integrity": "sha512-86oODyM2zajXwLs4Wok2mwVEfCwCnp756QyhLGX2IfsdRYr1DXLCgJgnLndaMUjJD7FBhnLk2okbNE9PdLxYRw==", "dev": true, "license": "MIT", "engines": { "node": ">= 18" }, "peerDependencies": { - "typedoc": "0.27.x" + "typedoc": "0.28.x" } }, "node_modules/typedoc/node_modules/minimatch": { @@ -15556,9 +15589,9 @@ } }, "node_modules/typescript": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", - "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -15574,6 +15607,12 @@ "dev": true, "license": "MIT" }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "license": "MIT", @@ -15751,14 +15790,15 @@ } }, "node_modules/webpack": { - "version": "5.98.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", - "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", + "version": "5.99.9", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz", + "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", @@ -15775,7 +15815,7 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.0", + "schema-utils": "^4.3.2", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", @@ -15887,12 +15927,12 @@ "peer": true }, "node_modules/whatwg-url": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", - "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "license": "MIT", "dependencies": { - "tr46": "^5.0.0", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { @@ -16103,15 +16143,15 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", - "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index df49d59..8289960 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sqlitecloud/drivers", - "version": "1.0.438", + "version": "1.0.507", "description": "SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -46,7 +46,7 @@ "eventemitter3": "^5.0.1", "lz4js": "^0.2.0", "socket.io-client": "^4.8.1", - "whatwg-url": "^14.1.1" + "whatwg-url": "^14.2.0" }, "peerDependencies": { "react-native-quick-base64": "*", @@ -57,21 +57,21 @@ "devDependencies": { "@types/jest": "^29.5.14", "@types/lz4": "^0.6.4", - "@types/node": "^22.13.8", + "@types/node": "^22.14.1", "@types/whatwg-url": "^13.0.0", - "dotenv": "^16.4.7", + "dotenv": "^16.5.0", "dotenv-cli": "^8.0.0", "husky": "^9.1.7", "jest": "^29.7.0", - "jest-html-reporter": "^4.0.1", + "jest-html-reporter": "^4.1.0", "prettier": "^3.5.3", "sqlite3": "^5.1.7", - "ts-jest": "^29.2.6", + "ts-jest": "^29.3.2", "ts-node": "^10.9.2", - "typedoc": "^0.27.9", - "typedoc-plugin-markdown": "^4.4.2", - "typescript": "^5.8.2", - "webpack": "^5.98.0", + "typedoc": "^0.28.2", + "typedoc-plugin-markdown": "^4.6.2", + "typescript": "^5.8.3", + "webpack": "^5.99.5", "webpack-cli": "^6.0.1" }, "config": {}, diff --git a/src/drivers/connection-tls.ts b/src/drivers/connection-tls.ts index ed872e7..e0ecc40 100644 --- a/src/drivers/connection-tls.ts +++ b/src/drivers/connection-tls.ts @@ -141,11 +141,11 @@ export class SQLiteCloudTlsConnection extends SQLiteCloudConnection { this.socket = undefined }, timeoutMs) - this.socket?.write(formattedCommands, 'utf-8', () => { + this.socket?.write(formattedCommands, () => { clearTimeout(timeout) // Clear the timeout on successful write }) } else { - this.socket?.write(formattedCommands, 'utf-8') + this.socket?.write(formattedCommands) } return this diff --git a/src/drivers/connection.ts b/src/drivers/connection.ts index 1b23699..17d6368 100644 --- a/src/drivers/connection.ts +++ b/src/drivers/connection.ts @@ -2,7 +2,7 @@ * connection.ts - base abstract class for sqlitecloud server connections */ -import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback, SQLiteCloudCommand } from './types' +import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback, SQLiteCloudCommand, SQLiteCloudDataTypes } from './types' import { validateConfiguration } from './utilities' import { OperationsQueue } from './queue' import { anonimizeCommand, getUpdateResults } from './utilities' diff --git a/src/drivers/database.ts b/src/drivers/database.ts index 80aa1f5..09cde31 100644 --- a/src/drivers/database.ts +++ b/src/drivers/database.ts @@ -22,6 +22,7 @@ import { SQLiteCloudArrayType, SQLiteCloudCommand, SQLiteCloudConfig, + SQLiteCloudDataTypes, SQLiteCloudError } from './types' import { isBrowser, popCallback } from './utilities' diff --git a/src/drivers/protocol.ts b/src/drivers/protocol.ts index 5d139b7..a021cb3 100644 --- a/src/drivers/protocol.ts +++ b/src/drivers/protocol.ts @@ -2,8 +2,8 @@ // protocol.ts - low level protocol handling for SQLiteCloud transport // -import { SQLiteCloudCommand, SQLiteCloudError, type SQLCloudRowsetMetadata, type SQLiteCloudDataTypes } from './types' import { SQLiteCloudRowset } from './rowset' +import { SAFE_INTEGER_MODE, SQLiteCloudCommand, SQLiteCloudError, type SQLCloudRowsetMetadata, type SQLiteCloudDataTypes } from './types' // explicitly importing buffer library to allow cross-platform support by replacing it import { Buffer } from 'buffer' @@ -302,7 +302,17 @@ export function popData(buffer: Buffer): { data: SQLiteCloudDataTypes | SQLiteCl // console.debug(`popData - dataType: ${dataType}, spaceIndex: ${spaceIndex}, commandLength: ${commandLength}, commandEnd: ${commandEnd}`) switch (dataType) { case CMD_INT: - return popResults(parseInt(buffer.subarray(1, spaceIndex).toString())) + // SQLite uses 64-bit INTEGER, but JS uses 53-bit Number + const value = BigInt(buffer.subarray(1, spaceIndex).toString()) + if (SAFE_INTEGER_MODE === 'bigint') { + return popResults(value) + } + if (SAFE_INTEGER_MODE === 'mixed') { + if (value <= BigInt(Number.MIN_SAFE_INTEGER) || BigInt(Number.MAX_SAFE_INTEGER) <= value) { + return popResults(value) + } + } + return popResults(Number(value)) case CMD_FLOAT: return popResults(parseFloat(buffer.subarray(1, spaceIndex).toString())) case CMD_NULL: @@ -334,7 +344,7 @@ export function popData(buffer: Buffer): { data: SQLiteCloudDataTypes | SQLiteCl } /** Format a command to be sent via SCSP protocol */ -export function formatCommand(command: SQLiteCloudCommand): string { +export function formatCommand(command: SQLiteCloudCommand): Buffer { // core returns null if there's a space after the semi column // we want to maintain a compatibility with the standard sqlite3 driver command.query = command.query.trim() @@ -346,22 +356,23 @@ export function formatCommand(command: SQLiteCloudCommand): string { return serializeData(command.query, false) } -function serializeCommand(data: SQLiteCloudDataTypes[], zeroString: boolean = false): string { +function serializeCommand(data: SQLiteCloudDataTypes[], zeroString: boolean = false): Buffer { const n = data.length - let serializedData = `${n} ` + let serializedData = Buffer.from(`${n} `) for (let i = 0; i < n; i++) { // the first string is the sql and it must be zero-terminated const zs = i == 0 || zeroString - serializedData += serializeData(data[i], zs) + serializedData = Buffer.concat([serializedData, serializeData(data[i], zs)]) } - const bytesTotal = Buffer.byteLength(serializedData, 'utf-8') - const header = `${CMD_ARRAY}${bytesTotal} ` - return header + serializedData + const bytesTotal = serializedData.byteLength + const header = Buffer.from(`${CMD_ARRAY}${bytesTotal} `) + + return Buffer.concat([header, serializedData]) } -function serializeData(data: SQLiteCloudDataTypes, zeroString: boolean = false): string { +function serializeData(data: SQLiteCloudDataTypes, zeroString: boolean = false): Buffer { if (typeof data === 'string') { let cmd = CMD_STRING if (zeroString) { @@ -370,24 +381,28 @@ function serializeData(data: SQLiteCloudDataTypes, zeroString: boolean = false): } const header = `${cmd}${Buffer.byteLength(data, 'utf-8')} ` - return header + data + return Buffer.from(header + data) } if (typeof data === 'number') { if (Number.isInteger(data)) { - return `${CMD_INT}${data} ` + return Buffer.from(`${CMD_INT}${data} `) } else { - return `${CMD_FLOAT}${data} ` + return Buffer.from(`${CMD_FLOAT}${data} `) } } + if (typeof data === 'bigint') { + return Buffer.from(`${CMD_INT}${data} `) + } + if (Buffer.isBuffer(data)) { - const header = `${CMD_BLOB}${data.length} ` - return header + data.toString('utf-8') + const header = `${CMD_BLOB}${data.byteLength} ` + return Buffer.concat([Buffer.from(header), data]) } if (data === null || data === undefined) { - return `${CMD_NULL} ` + return Buffer.from(`${CMD_NULL} `) } if (Array.isArray(data)) { diff --git a/src/drivers/types.ts b/src/drivers/types.ts index 6a1c7a3..8e9f9e1 100644 --- a/src/drivers/types.ts +++ b/src/drivers/types.ts @@ -10,6 +10,26 @@ export const DEFAULT_TIMEOUT = 300 * 1000 /** Default tls connection port */ export const DEFAULT_PORT = 8860 +/** + * Support to SQLite 64bit integer + * + * number - (default) always return Number type (max: 2^53 - 1) + * Precision is lost when selecting greater numbers from SQLite + * bigint - always return BigInt type (max: 2^63 - 1) for all numbers from SQLite + * (inlcuding `lastID` from WRITE statements) + * mixed - use BigInt and Number types depending on the value size + */ +export let SAFE_INTEGER_MODE = 'number' +if (typeof process !== 'undefined') { + SAFE_INTEGER_MODE = process.env['SAFE_INTEGER_MODE']?.toLowerCase() || 'number' +} +if (SAFE_INTEGER_MODE == 'bigint') { + console.debug('BigInt mode: Using Number for all INTEGER values from SQLite, including meta information from WRITE statements.') +} +if (SAFE_INTEGER_MODE == 'mixed') { + console.debug('Mixed mode: Using BigInt for INTEGER values from SQLite (including meta information from WRITE statements) bigger then 2^53, Number otherwise.') +} + /** * Configuration for SQLite cloud connection * @note Options are all lowecase so they 1:1 compatible with C SDK @@ -26,6 +46,8 @@ export interface SQLiteCloudConfig { password_hashed?: boolean /** API key can be provided instead of username and password */ apikey?: string + /** Access Token provided in place of API Key or username/password */ + token?: string /** Host name is required unless connectionstring is provided, eg: xxx.sqlitecloud.io */ host?: string diff --git a/src/drivers/utilities.ts b/src/drivers/utilities.ts index b7075ab..5573adf 100644 --- a/src/drivers/utilities.ts +++ b/src/drivers/utilities.ts @@ -2,12 +2,10 @@ // utilities.ts - utility methods to manipulate SQL statements // -import { SQLiteCloudConfig, SQLiteCloudError, SQLiteCloudDataTypes, DEFAULT_PORT, DEFAULT_TIMEOUT } from './types' -import { SQLiteCloudArrayType } from './types' +import { DEFAULT_PORT, DEFAULT_TIMEOUT, SQLiteCloudArrayType, SQLiteCloudConfig, SQLiteCloudDataTypes, SQLiteCloudError } from './types' // explicitly importing these libraries to allow cross-platform support by replacing them import { URL } from 'whatwg-url' -import { Buffer } from 'buffer' // // determining running environment, thanks to browser-or-node @@ -42,45 +40,47 @@ export function anonimizeError(error: Error): Error { export function getInitializationCommands(config: SQLiteCloudConfig): string { // we check the credentials using non linearizable so we're quicker // then we bring back linearizability unless specified otherwise - let commands = 'SET CLIENT KEY NONLINEARIZABLE TO 1; ' + let commands = 'SET CLIENT KEY NONLINEARIZABLE TO 1;' // first user authentication, then all other commands if (config.apikey) { - commands += `AUTH APIKEY ${config.apikey}; ` + commands += `AUTH APIKEY ${config.apikey};` + } else if (config.token) { + commands += `AUTH TOKEN ${config.token};` } else { - commands += `AUTH USER ${config.username || ''} ${config.password_hashed ? 'HASH' : 'PASSWORD'} ${config.password || ''}; ` + commands += `AUTH USER ${config.username || ''} ${config.password_hashed ? 'HASH' : 'PASSWORD'} ${config.password || ''};` } if (config.compression) { - commands += 'SET CLIENT KEY COMPRESSION TO 1; ' + commands += 'SET CLIENT KEY COMPRESSION TO 1;' } if (config.zerotext) { - commands += 'SET CLIENT KEY ZEROTEXT TO 1; ' + commands += 'SET CLIENT KEY ZEROTEXT TO 1;' } if (config.noblob) { - commands += 'SET CLIENT KEY NOBLOB TO 1; ' + commands += 'SET CLIENT KEY NOBLOB TO 1;' } if (config.maxdata) { - commands += `SET CLIENT KEY MAXDATA TO ${config.maxdata}; ` + commands += `SET CLIENT KEY MAXDATA TO ${config.maxdata};` } if (config.maxrows) { - commands += `SET CLIENT KEY MAXROWS TO ${config.maxrows}; ` + commands += `SET CLIENT KEY MAXROWS TO ${config.maxrows};` } if (config.maxrowset) { - commands += `SET CLIENT KEY MAXROWSET TO ${config.maxrowset}; ` + commands += `SET CLIENT KEY MAXROWSET TO ${config.maxrowset};` } // we ALWAYS set non linearizable to 1 when we start so we can be quicker on login // but then we need to put it back to its default value if "linearizable" unless set if (!config.non_linearizable) { - commands += 'SET CLIENT KEY NONLINEARIZABLE TO 0; ' + commands += 'SET CLIENT KEY NONLINEARIZABLE TO 0;' } if (config.database) { if (config.create && !config.memory) { - commands += `CREATE DATABASE ${config.database} IF NOT EXISTS; ` + commands += `CREATE DATABASE ${config.database} IF NOT EXISTS;` } - commands += `USE DATABASE ${config.database}; ` + commands += `USE DATABASE ${config.database};` } return commands @@ -109,13 +109,12 @@ export function getUpdateResults(results?: any): Record | undefined switch (results[0]) { case SQLiteCloudArrayType.ARRAY_TYPE_SQLITE_EXEC: return { - type: results[0], - index: results[1], + type: Number(results[0]), + index: Number(results[1]), lastID: results[2], // ROWID (sqlite3_last_insert_rowid) changes: results[3], // CHANGES(sqlite3_changes) totalChanges: results[4], // TOTAL_CHANGES (sqlite3_total_changes) - finalized: results[5], // FINALIZED - // + finalized: Number(results[5]), // FINALIZED rowId: results[2] // same as lastId } } @@ -177,16 +176,19 @@ export function validateConfiguration(config: SQLiteCloudConfig): SQLiteCloudCon config.non_linearizable = parseBoolean(config.non_linearizable) config.insecure = parseBoolean(config.insecure) - const hasCredentials = (config.username && config.password) || config.apikey + const hasCredentials = (config.username && config.password) || config.apikey || config.token if (!config.host || !hasCredentials) { console.error('SQLiteCloudConnection.validateConfiguration - missing arguments', config) - throw new SQLiteCloudError('The user, password and host arguments or the ?apikey= must be specified.', { errorCode: 'ERR_MISSING_ARGS' }) + throw new SQLiteCloudError('The user, password and host arguments, the ?apikey= or the ?token= must be specified.', { errorCode: 'ERR_MISSING_ARGS' }) } if (!config.connectionstring) { // build connection string from configuration, values are already validated + config.connectionstring = `sqlitecloud://${config.host}:${config.port}/${config.database || ''}` if (config.apikey) { - config.connectionstring = `sqlitecloud://${config.host}:${config.port}/${config.database || ''}?apikey=${config.apikey}` + config.connectionstring += `?apikey=${config.apikey}` + } else if (config.token) { + config.connectionstring += `?token=${config.token}` } else { config.connectionstring = `sqlitecloud://${encodeURIComponent(config.username || '')}:${encodeURIComponent(config.password || '')}@${config.host}:${ config.port @@ -215,13 +217,13 @@ export function parseconnectionstring(connectionstring: string): SQLiteCloudConf // all lowecase options const options: { [key: string]: string } = {} url.searchParams.forEach((value, key) => { - options[key.toLowerCase().replace(/-/g, '_')] = value + options[key.toLowerCase().replace(/-/g, '_')] = value.trim() }) const config: SQLiteCloudConfig = { ...options, - username: decodeURIComponent(url.username), - password: decodeURIComponent(url.password), + username: url.username ? decodeURIComponent(url.username) : undefined, + password: url.password ? decodeURIComponent(url.password) : undefined, password_hashed: options.password_hashed ? parseBoolean(options.password_hashed) : undefined, host: url.hostname, // type cast values @@ -241,13 +243,10 @@ export function parseconnectionstring(connectionstring: string): SQLiteCloudConf verbose: options.verbose ? parseBoolean(options.verbose) : undefined } - // either you use an apikey or username and password - if (config.apikey) { - if (config.username || config.password) { - console.warn('SQLiteCloudConnection.parseconnectionstring - apikey and username/password are both specified, using apikey') - } - delete config.username - delete config.password + // either you use an apikey, token or username and password + if (Number(!!config.apikey) + Number(!!config.token) + Number(!!(config.username || config.password)) > 1) { + console.error('SQLiteCloudConnection.parseconnectionstring - choose between apikey, token or username/password') + throw new SQLiteCloudError('Choose between apikey, token or username/password') } const database = url.pathname.replace('/', '') // pathname is database name, remove the leading slash @@ -257,7 +256,7 @@ export function parseconnectionstring(connectionstring: string): SQLiteCloudConf return config } catch (error) { - throw new SQLiteCloudError(`Invalid connection string: ${connectionstring}`) + throw new SQLiteCloudError(`Invalid connection string: ${connectionstring} - error: ${error}`) } } diff --git a/test/compare.test.ts b/test/compare.test.ts index 9656440..1ea1512 100644 --- a/test/compare.test.ts +++ b/test/compare.test.ts @@ -2,12 +2,11 @@ * compare.test.ts - test driver api against sqlite3 equivalents */ -import { CHINOOK_DATABASE_FILE, CHINOOK_FIRST_TRACK, LONG_TIMEOUT, removeDatabase, TESTING_SQL } from './shared' -import { getChinookDatabase, getTestingDatabase } from './shared' +import { CHINOOK_DATABASE_FILE, CHINOOK_FIRST_TRACK, getChinookDatabase, getTestingDatabase, LONG_TIMEOUT, removeDatabase, TESTING_SQL } from './shared' // https://github.com/TryGhost/node-sqlite3/wiki/API -import sqlite3 from 'sqlite3' import { join } from 'path' +import sqlite3 from 'sqlite3' const INSERT_SQL = "INSERT INTO people (name, hobby, age) VALUES ('Fantozzi Ugo', 'Competitive unicorn farting', 42); " const TESTING_DATABASE_FILE = join(__dirname, 'assets/testing.db') @@ -252,6 +251,29 @@ describe('Database.get', () => { }) }) + it('should handle big integer (>= 2^53) as Number by default', done => { + const chinookCloud = getChinookDatabase() + const chinookFile = getChinookDatabaseFile() + + const sql = 'SELECT 5876026397369593117 as bignumber;' + chinookCloud.get(sql, (cloudError, cloudRow) => { + expect(cloudError).toBeFalsy() + expect(typeof (cloudRow as any)['bignumber']).toBe('number') + expect((cloudRow as any)['bignumber']).toBe(5876026397369593000) + + chinookFile.get(sql, (fileError, fileRow) => { + expect(fileError).toBeNull() + // node-sqlite3 does not support bigint: https://github.com/TryGhost/node-sqlite3/issues/922 + expect(typeof (fileRow as any)['bignumber']).toBe('number') + expect((fileRow as any)['bignumber']).toBe(5876026397369593000) + + chinookCloud.close() + chinookFile.close() + done() + }) + }) + }) + // end get }) diff --git a/test/connection-tls.test.ts b/test/connection-tls.test.ts index 8425004..c72cc34 100644 --- a/test/connection-tls.test.ts +++ b/test/connection-tls.test.ts @@ -53,19 +53,19 @@ describe('connect', () => { }) */ -it( - 'should connect with config object string', - done => { - const configObj = getChinookConfig() - const connection = new SQLiteCloudTlsConnection(configObj, error => { - expect(error).toBeNull() - expect(connection.connected).toBe(true) - connection.close() - done() - }) - }, - LONG_TIMEOUT -) + it( + 'should connect with config object string', + done => { + const configObj = getChinookConfig() + const connection = new SQLiteCloudTlsConnection(configObj, error => { + expect(error).toBeNull() + expect(connection.connected).toBe(true) + connection.close() + done() + }) + }, + LONG_TIMEOUT + ) it( 'should connect with config object string and test command', @@ -174,7 +174,7 @@ it( expect(error).toBeDefined() expect(error).toBeInstanceOf(SQLiteCloudError) const sqliteCloudError = error as SQLiteCloudError - expect(sqliteCloudError.message).toBe('The user, password and host arguments or the ?apikey= must be specified.') + expect(sqliteCloudError.message).toBe('The user, password and host arguments, the ?apikey= or the ?token= must be specified.') expect(sqliteCloudError.errorCode).toBe('ERR_MISSING_ARGS') expect(sqliteCloudError.externalErrorCode).toBeUndefined() expect(sqliteCloudError.offsetCode).toBeUndefined() @@ -324,6 +324,23 @@ describe('send test commands', () => { LONG_TIMEOUT ) + it('should use Number as default behavior for integer INT64', done => { + const chinook = getConnection() + chinook.sendCommands('TEST INT64', (error, results) => { + let err = null + try { + expect(error).toBeNull() + expect(typeof results).toBe('number') + expect(results).toBe(9223372036854776000) + } catch (error) { + err = error + } finally { + chinook.close() + err ? done(err) : done() + } + }) + }) + it('should test null', done => { const connection = getConnection() connection.sendCommands('TEST NULL', (error, results) => { diff --git a/test/database.test.ts b/test/database.test.ts index ace2400..b1e092d 100644 --- a/test/database.test.ts +++ b/test/database.test.ts @@ -6,6 +6,7 @@ import { describe, expect, it } from '@jest/globals' import { RowCountCallback } from '../src/drivers/types' import { Database, SQLiteCloudError, SQLiteCloudRow, SQLiteCloudRowset, sanitizeSQLiteIdentifier } from '../src/index' import { LONG_TIMEOUT, getChinookDatabase, getTestingDatabase, getTestingDatabaseAsync, removeDatabase, removeDatabaseAsync } from './shared' +const crypto = require('crypto') // // utility methods to setup and destroy temporary test databases @@ -266,7 +267,7 @@ describe('Database.get', () => { }) }) - // call close() right after the execution + // call close() right after the execution // of the query not in its callback chinook.close(error => { expect(error).toBeNull() @@ -604,6 +605,45 @@ describe('Database.sql (async)', () => { await removeDatabaseAsync(database) } }) + + it('binding should work with blob', async () => { + let database + try { + database = await getTestingDatabaseAsync() + const hash = crypto.createHash('sha256').update('my blob data').digest() + + await database.sql('CREATE TABLE IF NOT EXISTS blobs (id INTEGER PRIMARY KEY, hash BLOB NOT NULL, myindex INTEGER NOT NULL);') + let results = await database.sql('INSERT INTO blobs (hash, myindex) VALUES (?, ?);', hash, 1) + expect(results.changes).toEqual(1) + + results = await database.sql('SELECT * FROM blobs WHERE id = ? and hash = ?;', results.lastID, hash) + + expect(results).toHaveLength(1) + expect(results[0].hash).toEqual(hash) + expect(results[0].myindex).toEqual(1) + } finally { + await removeDatabaseAsync(database) + } + }) + + it('should insert bigInt value as a full 64 bit integer', async () => { + let database + try { + database = await getTestingDatabaseAsync() + const bigIntValue = BigInt(2 ** 63) - BigInt(1) // 9223372036854775807 + + await database.sql('CREATE TABLE IF NOT EXISTS bigints (id INTEGER PRIMARY KEY, value BIGINT NOT NULL);') + const meta = await database.sql('INSERT INTO bigints (value) VALUES (?);', bigIntValue) + + // by default, BigInt values are returned as Number loosing precision + // retrieving it as text preserves the value + const results = await database.sql('SELECT CAST(value AS TEXT) as bigIntAsString FROM bigints WHERE id = ?;', meta.lastID) + expect(results).toHaveLength(1) + expect(results[0].bigIntAsString).toEqual(bigIntValue.toString()) + } finally { + await removeDatabaseAsync(database) + } + }) }) it('should be connected', async () => { diff --git a/test/protocol.test.ts b/test/protocol.test.ts index cf80f2b..0e9f3fd 100644 --- a/test/protocol.test.ts +++ b/test/protocol.test.ts @@ -36,13 +36,13 @@ const testCases = [ { query: 'SELECT ?, ?, ?, ?, ?', parameters: ['world', 123, 3.14, null, Buffer.from('hello')], - expected: '=57 6 !21 SELECT ?, ?, ?, ?, ?\x00!6 world\x00:123 ,3.14 _ $5 hello', + expected: '=57 6 !21 SELECT ?, ?, ?, ?, ?\x00!6 world\x00:123 ,3.14 _ $5 hello' }, { query: 'SELECT ?', parameters: ["'hello world'"], - expected: "=32 2 !9 SELECT ?\x00!14 'hello world'\x00", - }, + expected: "=32 2 !9 SELECT ?\x00!14 'hello world'\x00" + } ] describe('Format command', () => { @@ -50,7 +50,7 @@ describe('Format command', () => { it(`should serialize ${JSON.stringify([query, ...parameters])}`, () => { const command: SQLiteCloudCommand = { query, parameters } const serialized = formatCommand(command) - expect(serialized).toEqual(expected) + expect(serialized).toEqual(Buffer.from(expected)) }) }) }) diff --git a/test/utilities.test.ts b/test/utilities.test.ts index 423b3ca..f3e26b5 100644 --- a/test/utilities.test.ts +++ b/test/utilities.test.ts @@ -3,7 +3,7 @@ // import { SQLiteCloudError } from '../src/index' -import { parseconnectionstring, sanitizeSQLiteIdentifier } from '../src/drivers/utilities' +import { getInitializationCommands, parseconnectionstring, sanitizeSQLiteIdentifier } from '../src/drivers/utilities' import { getTestingDatabaseName } from './shared' import { expect, describe, it } from '@jest/globals' @@ -162,6 +162,21 @@ describe('parseconnectionstring', () => { expect(config.timeout).toBe(123) }) + + it('expect error when both user/pass and api key are set', () => { + const connectionstring = 'sqlitecloud://user:password@host:1234/database?apikey=yyy' + expect(() => parseconnectionstring(connectionstring)).toThrowError('Choose between apikey, token or username/password') + }) + + it('expect error when both user/pass and token are set', () => { + const connectionstring = 'sqlitecloud://user:password@host:1234/database?token=yyy' + expect(() => parseconnectionstring(connectionstring)).toThrowError('Choose between apikey, token or username/password') + }) + + it('expect error when both apikey and token are set', () => { + const connectionstring = 'sqlitecloud://host:1234/database?apikey=xxx&token=yyy' + expect(() => parseconnectionstring(connectionstring)).toThrowError('Choose between apikey, token or username/password') + }) }) describe('getTestingDatabaseName', () => { @@ -190,3 +205,17 @@ describe('sanitizeSQLiteIdentifier()', () => { expect(sanitized).toBe('"chinook.sql; DROP TABLE \"\"albums\"\""') }) }) + +describe('getInitializationCommands()', () => { + it('should return commands with auth token command', () => { + const config = { + token: 'mytoken', + database: 'mydb', + } + + const result = getInitializationCommands(config) + + expect(result).toContain('AUTH TOKEN mytoken;') + expect(result).not.toContain('AUTH APIKEY') + }) +}) \ No newline at end of file