├── .eslintignore ├── .eslintrc.yaml ├── .gitignore ├── README.md ├── dprint.json ├── package.json ├── pnpm-lock.yaml ├── scripts └── build.ts ├── src ├── duckdb-neo │ ├── result-transformers.ts │ └── utils.ts ├── index.ts ├── neo.ts ├── pool-ts │ ├── DefaultEvictor.ts │ ├── Deferred.ts │ ├── Deque.ts │ ├── DequeIterator.ts │ ├── DoublyLinkedList.ts │ ├── DoublyLinkedListIterator.ts │ ├── Pool.ts │ ├── PoolDefaults.ts │ ├── PoolOptions.ts │ ├── PooledResource.ts │ ├── PriorityQueue.ts │ ├── Queue.ts │ ├── ResourceLoan.ts │ ├── ResourceRequest.ts │ ├── errors.ts │ ├── factoryValidator.ts │ ├── types.ts │ └── utils.ts ├── recycling-pool.ts ├── sql-template-default.ts ├── sql-template-neo.ts ├── sql-template.ts ├── types.ts └── utils.ts ├── tests ├── neo-waddler-unit.test.ts ├── recycling-pool.test.ts ├── waddler-errors.test.ts └── waddler-unit.test.ts ├── tsconfig.json └── vitest.config.ts /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: 3 | - "eslint:recommended" 4 | - "plugin:@typescript-eslint/recommended" 5 | - "plugin:unicorn/recommended" 6 | parser: "@typescript-eslint/parser" 7 | parserOptions: 8 | project: "./tsconfig.json" 9 | plugins: 10 | - import 11 | - unused-imports 12 | overrides: 13 | - files: 14 | - "**/tests/**/*.ts" 15 | rules: 16 | import/extensions: "off" 17 | rules: 18 | "@typescript-eslint/consistent-type-imports": 19 | - error 20 | - disallowTypeAnnotations: false 21 | fixStyle: separate-type-imports 22 | "@typescript-eslint/no-import-type-side-effects": "error" 23 | import/no-cycle: error 24 | import/no-self-import: error 25 | import/no-empty-named-blocks: error 26 | unused-imports/no-unused-imports: error 27 | import/no-useless-path-segments: error 28 | import/newline-after-import: error 29 | import/no-duplicates: error 30 | import/extensions: 31 | - error 32 | - always 33 | - ignorePackages: true 34 | "@typescript-eslint/no-explicit-any": "off" 35 | "@typescript-eslint/no-non-null-assertion": "off" 36 | "@typescript-eslint/no-namespace": "off" 37 | "@typescript-eslint/no-unused-vars": 38 | - error 39 | - argsIgnorePattern: "^_" 40 | varsIgnorePattern: "^_" 41 | "@typescript-eslint/no-this-alias": "off" 42 | "@typescript-eslint/no-var-requires": "off" 43 | "unicorn/prefer-node-protocol": "off" 44 | "unicorn/prefer-top-level-await": "off" 45 | "unicorn/prevent-abbreviations": "off" 46 | "unicorn/prefer-switch": "off" 47 | "unicorn/catch-error-name": "off" 48 | "unicorn/no-null": "off" 49 | "unicorn/numeric-separators-style": "off" 50 | "unicorn/explicit-length-check": "off" 51 | "unicorn/filename-case": "off" 52 | "unicorn/prefer-module": "off" 53 | "unicorn/no-array-reduce": "off" 54 | "unicorn/no-nested-ternary": "off" 55 | "unicorn/no-useless-undefined": 56 | - error 57 | - checkArguments: false 58 | "unicorn/no-this-assignment": "off" 59 | "unicorn/empty-brace-spaces": "off" 60 | "unicorn/no-thenable": "off" 61 | "unicorn/consistent-function-scoping": "off" 62 | "unicorn/prefer-type-error": "off" 63 | "unicorn/relative-url-style": "off" 64 | "eqeqeq": "error" 65 | "unicorn/prefer-string-replace-all": "off" 66 | "unicorn/no-process-exit": "off" 67 | "@typescript-eslint/ban-ts-comment": "off" 68 | "@typescript-eslint/no-empty-interface": "off" 69 | "@typescript-eslint/no-unsafe-declaration-merging": "off" 70 | "no-inner-declarations": "off" 71 | ignorePatterns: ["src/test.ts"] 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | db 4 | src/test.ts 5 | src/db-test.ts 6 | dist 7 | package.tgz 8 | .DS_Store 9 | seedFromParquet.ts 10 | storage 11 | nodejs_connection_pool_* 12 | Limitations.md 13 | waddlerChangelog.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Waddler 🦆 2 | Website • 3 | Documentation • 4 | Twitter • by [Drizzle Team](https://drizzle.team) 5 | 6 | Waddler is a thin SQL client on top of official DuckDB NodeJS driver with modern API inspired by `postgresjs` and based on ES6 Tagged Template Strings. 7 | 8 | Waddler has a baked in database pooling which unlocks full potential of hosted DuckDB services like MotherDuck. It does create multiple database instances under the hood and lets you concurrently fetch data from the remote MotherDuck database. 9 | 10 | ```ts 11 | import { waddler } from "waddler"; 12 | 13 | const sql = waddler({ dbUrl: ":memory:" }); 14 | const sql = waddler({ dbUrl: "file.db" }); 15 | const sql = waddler({ dbUrl: "md?:" }); // mother duck url 16 | const sql = waddler({ dbUrl: "md?:", min: 1, max: 8 }); // automatic database pooling 17 | 18 | // promisified SQL template API 19 | const result = await sql`select * from users`; 20 | 21 | // no SQL injections 22 | await sql`select * from users where id = ${10}`; // <-- converts to $1 and [10] params 23 | 24 | // waddler supports types 25 | await sql<{ id: number, name: string }>`select * from users`; 26 | 27 | // streaming and chunking 28 | const stream = sql`select * from users`.stream(); 29 | for await (const row of stream) { 30 | console.log(row); 31 | } 32 | 33 | const chunked = sql`select * from users`.chunked(2); 34 | for await (const chunk of chunked) { 35 | console.log(chunk); 36 | } 37 | 38 | // and many more, checkout at waddler.drizzle.team 39 | ``` 40 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript": { 3 | "useTabs": true, 4 | "quoteStyle": "preferSingle", 5 | "quoteProps": "asNeeded", 6 | "arrowFunction.useParentheses": "force", 7 | "jsx.quoteStyle": "preferSingle" 8 | }, 9 | "json": { 10 | "useTabs": true 11 | }, 12 | "markdown": {}, 13 | "includes": ["**/*.{ts,tsx,js,jsx,cjs,mjs,json}"], 14 | "excludes": ["**/node_modules", "dist"], 15 | "plugins": [ 16 | "https://plugins.dprint.dev/typescript-0.91.1.wasm", 17 | "https://plugins.dprint.dev/json-0.19.3.wasm", 18 | "https://plugins.dprint.dev/markdown-0.17.1.wasm" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "waddler", 3 | "version": "0.0.12", 4 | "main": "index.js", 5 | "type": "module", 6 | "repository": "https://github.com/drizzle-team/waddler", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "npx tsx ./src/test.ts", 10 | "start:db": "npx tsx ./src/db-test.ts ", 11 | "test": "vitest --config ./vitest.config.ts", 12 | "lint": "concurrently -n eslint,dprint \"eslint --ext ts .\" \"dprint check --list-different\"", 13 | "build": "pnpm tsx scripts/build.ts", 14 | "pack": "(cd dist && npm pack --pack-destination ..) && rm -f package.tgz && mv *.tgz package.tgz" 15 | }, 16 | "exports": { 17 | ".": { 18 | "import": { 19 | "types": "./index.d.ts", 20 | "default": "./index.js" 21 | }, 22 | "require": { 23 | "types": "./index.d.cts", 24 | "default": "./index.cjs" 25 | } 26 | }, 27 | "./neo": { 28 | "import": { 29 | "types": "./neo.d.ts", 30 | "default": "./neo.js" 31 | }, 32 | "require": { 33 | "types": "./neo.d.cts", 34 | "default": "./neo.cjs" 35 | } 36 | } 37 | }, 38 | "devDependencies": { 39 | "@arethetypeswrong/cli": "^0.16.4", 40 | "@duckdb/node-api": "1.1.2-alpha.4", 41 | "@types/node": "^22.7.6", 42 | "@typescript-eslint/eslint-plugin": "^8.10.0", 43 | "concurrently": "^8.2.1", 44 | "dprint": "^0.46.2", 45 | "duckdb": "^1.1.1", 46 | "eslint": "^8.50.0", 47 | "eslint-plugin-import": "^2.31.0", 48 | "eslint-plugin-no-instanceof": "^1.0.1", 49 | "eslint-plugin-unicorn": "^56.0.0", 50 | "eslint-plugin-unused-imports": "^4.1.4", 51 | "tsup": "^8.3.5", 52 | "tsx": "^4.19.1", 53 | "typescript": "^5.5.4", 54 | "vitest": "^2.1.2", 55 | "zx": "^8.1.9" 56 | }, 57 | "peerDependencies": { 58 | "@duckdb/node-api": "^1.1.2-alpha.4", 59 | "duckdb": "^1.1.1" 60 | }, 61 | "peerDependenciesMeta": { 62 | "duckdb": { 63 | "optional": true 64 | }, 65 | "@duckdb/node-api": { 66 | "optional": true 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | import 'zx/globals'; 2 | 3 | import { build } from 'tsup'; 4 | 5 | fs.removeSync('dist'); 6 | 7 | await build({ 8 | entry: ['src/index.ts'], 9 | splitting: false, 10 | sourcemap: true, 11 | dts: true, 12 | format: ['cjs', 'esm'], 13 | bundle: true, 14 | outExtension(ctx) { 15 | if (ctx.format === 'cjs') { 16 | return { 17 | dts: '.d.cts', 18 | js: '.cjs', 19 | }; 20 | } 21 | return { 22 | dts: '.d.ts', 23 | js: '.js', 24 | }; 25 | }, 26 | }); 27 | 28 | await build({ 29 | entry: ['src/neo.ts'], 30 | splitting: false, 31 | sourcemap: true, 32 | dts: true, 33 | format: ['cjs', 'esm'], 34 | bundle: true, 35 | outExtension(ctx) { 36 | if (ctx.format === 'cjs') { 37 | return { 38 | dts: '.d.cts', 39 | js: '.cjs', 40 | }; 41 | } 42 | return { 43 | dts: '.d.ts', 44 | js: '.js', 45 | }; 46 | }, 47 | }); 48 | 49 | fs.copyFileSync('package.json', 'dist/package.json'); 50 | fs.copyFileSync('README.md', 'dist/README.md'); 51 | -------------------------------------------------------------------------------- /src/duckdb-neo/result-transformers.ts: -------------------------------------------------------------------------------- 1 | import type { DuckDBDataChunk, DuckDBResult, DuckDBVector } from '@duckdb/node-api'; 2 | import { transformValue } from './utils.ts'; 3 | 4 | export const transformResultToArrays = async (result: DuckDBResult) => { 5 | const data: any[][] = []; 6 | const chunks = await result.fetchAllChunks(); 7 | for (const chunk of chunks) { 8 | const columnVectors = getColumnVectors(chunk); 9 | 10 | for (let rowIndex = 0; rowIndex < chunk.rowCount; rowIndex++) { 11 | const row: any[] = []; 12 | 13 | for (const [columnIndex, columnVector] of columnVectors.entries()) { 14 | let value = columnVector.getItem(rowIndex); 15 | const columnType = result.columnType(columnIndex); 16 | value = transformValue(value, columnType); 17 | 18 | row.push(value); 19 | } 20 | data.push(row); 21 | } 22 | } 23 | 24 | return data; 25 | }; 26 | 27 | export const transformResultToObjects = async (result: DuckDBResult) => { 28 | const data: { 29 | [columnName: string]: any; 30 | }[] = []; 31 | 32 | const chunks = await result.fetchAllChunks(); 33 | for (const chunk of chunks) { 34 | const columnVectors = getColumnVectors(chunk); 35 | 36 | for (let rowIndex = 0; rowIndex < chunk.rowCount; rowIndex++) { 37 | const row = transformResultRowToObject(result, columnVectors, rowIndex); 38 | data.push(row); 39 | } 40 | } 41 | 42 | return data; 43 | }; 44 | 45 | export const transformResultRowToObject = ( 46 | result: DuckDBResult, 47 | columnVectors: DuckDBVector[], 48 | rowIndex: number, 49 | ) => { 50 | const row: { [key: string]: any } = {}; 51 | 52 | for (const [columnIndex, columnVector] of columnVectors.entries()) { 53 | let value = columnVector.getItem(rowIndex); 54 | const columnType = result.columnType(columnIndex); 55 | value = transformValue(value, columnType); 56 | const colName = result.columnName(columnIndex); 57 | 58 | row[colName] = value; 59 | } 60 | 61 | return row; 62 | }; 63 | 64 | export const getColumnVectors = (chunk: DuckDBDataChunk) => { 65 | const columnVectors: DuckDBVector[] = []; 66 | 67 | for (let columnIndex = 0; columnIndex < chunk.columnCount; columnIndex++) { 68 | const columnVector = chunk.getColumnVector(columnIndex); 69 | columnVectors.push(columnVector); 70 | } 71 | 72 | return columnVectors; 73 | }; 74 | -------------------------------------------------------------------------------- /src/duckdb-neo/utils.ts: -------------------------------------------------------------------------------- 1 | import type { DuckDBArrayType, DuckDBListType, DuckDBPreparedStatement, DuckDBType } from '@duckdb/node-api'; 2 | import { 3 | DuckDBArrayValue, 4 | DuckDBDateValue, 5 | DuckDBListValue, 6 | DuckDBMapValue, 7 | DuckDBStructValue, 8 | DuckDBTimestampValue, 9 | DuckDBTimeValue, 10 | DuckDBUnionValue, 11 | } from '@duckdb/node-api'; 12 | import type { UnsafeParamType } from '../types.ts'; 13 | import { stringifyArray } from '../utils.ts'; 14 | 15 | // Insert params 16 | const MIN_INT32 = -2147483648; 17 | const MAX_INT32 = 2147483647; 18 | 19 | export const bindParams = (prepared: DuckDBPreparedStatement, params: UnsafeParamType[]) => { 20 | for (const [idx, param] of params.entries()) { 21 | if (param === undefined) { 22 | throw new Error("you can't specify undefined as parameter."); 23 | } 24 | if (typeof param === 'string') { 25 | prepared.bindVarchar(idx + 1, param); 26 | continue; 27 | } 28 | 29 | if (typeof param === 'bigint') { 30 | prepared.bindBigInt(idx + 1, param); 31 | continue; 32 | } 33 | 34 | if (typeof param === 'boolean') { 35 | prepared.bindBoolean(idx + 1, param); 36 | continue; 37 | } 38 | 39 | if (param === null) { 40 | prepared.bindNull(idx + 1); 41 | continue; 42 | } 43 | 44 | if (typeof param === 'number') { 45 | if (Number.isInteger(param)) { 46 | if (param >= MIN_INT32 && param <= MAX_INT32) { 47 | prepared.bindInteger(idx + 1, param); 48 | continue; 49 | } 50 | 51 | prepared.bindBigInt(idx + 1, BigInt(param)); 52 | continue; 53 | } 54 | 55 | prepared.bindDouble(idx + 1, param); 56 | continue; 57 | } 58 | 59 | if (param instanceof Date) { 60 | prepared.bindTimestamp(idx + 1, new DuckDBTimestampValue(BigInt(param.getTime() * 1000))); 61 | continue; 62 | } 63 | 64 | if (typeof param === 'object') { 65 | if (Array.isArray(param)) { 66 | prepared.bindVarchar(idx + 1, stringifyArray(param)); 67 | continue; 68 | } 69 | 70 | prepared.bindVarchar(idx + 1, JSON.stringify(param)); 71 | continue; 72 | } 73 | } 74 | }; 75 | 76 | // select 77 | const transformIsNeeded = (value: any, columnType?: DuckDBType) => { 78 | const valueTypes = new Set(['string', 'boolean', 'number', 'bigint']); 79 | if ((valueTypes.has(typeof value) && columnType === undefined) || value === null) { 80 | return false; 81 | } 82 | 83 | return true; 84 | }; 85 | 86 | export const transformValue = (value: any, columnType?: DuckDBType | undefined) => { 87 | if (transformIsNeeded(value, columnType) === false) { 88 | return value; 89 | } 90 | 91 | if (value instanceof DuckDBDateValue) { 92 | // value in days 93 | return (new Date(value.days * 24 * 60 * 60 * 1000)); 94 | } 95 | 96 | if (value instanceof DuckDBTimeValue) { 97 | // typeof value === "bigint"; time in microseconds since 00:00:00 98 | let microS = Number(value.micros); 99 | const microSInHour = 1000 * 1000 * 60 * 60; 100 | const hours = Math.floor(microS / microSInHour); 101 | 102 | microS = microS - (hours * microSInHour); 103 | const microSInMinute = 1000 * 1000 * 60; 104 | const minutes = Math.floor(microS / microSInMinute); 105 | 106 | microS = microS - (minutes * microSInMinute); 107 | const microSInSecond = 1000 * 1000; 108 | const seconds = Math.floor(microS / microSInSecond); 109 | 110 | microS = microS - (seconds * microSInSecond); 111 | const milliS = Math.floor(microS / 1000); 112 | 113 | return (`${hours}:${minutes}:${seconds}.${milliS}`); 114 | } 115 | 116 | if (value instanceof DuckDBTimestampValue) { 117 | // typeof value === "bigint" 118 | return (new Date(Number((value.micros as bigint) / BigInt(1000)))); 119 | } 120 | 121 | if ( 122 | value instanceof DuckDBListValue || value instanceof DuckDBArrayValue 123 | ) { 124 | const transformedArray = transformNDList(value, columnType as DuckDBListType | DuckDBArrayType | undefined); 125 | return transformedArray; 126 | } 127 | 128 | if (value instanceof DuckDBMapValue) { 129 | const valueMap = new Map(); 130 | 131 | for (const valueI of value.entries) { 132 | valueMap.set( 133 | transformValue(valueI.key), 134 | transformValue(valueI.value), 135 | ); 136 | } 137 | 138 | return valueMap; 139 | } 140 | 141 | if (typeof value === 'string' && columnType?.alias === 'JSON') { 142 | return JSON.parse(value); 143 | } 144 | 145 | if (value instanceof DuckDBStructValue) { 146 | const valueStruct: { [name: string]: any } = {}; 147 | for (const [keyI, valueI] of Object.entries(value.entries)) { 148 | valueStruct[keyI] = transformValue(valueI, columnType); 149 | } 150 | 151 | return valueStruct; 152 | } 153 | 154 | if (value instanceof DuckDBUnionValue) { 155 | const transformedValue: any = transformValue(value.value); 156 | return transformedValue; 157 | } 158 | 159 | return value; 160 | }; 161 | 162 | export const transformNDList = ( 163 | listValue: DuckDBListValue | DuckDBArrayValue | any, 164 | columnType?: DuckDBListType | DuckDBArrayType | any, 165 | ): any[] => { 166 | if (!(listValue instanceof DuckDBListValue) && !(listValue instanceof DuckDBArrayValue)) { 167 | return transformValue(listValue, columnType); 168 | } 169 | 170 | const nDList = []; 171 | for (const item of listValue.items) { 172 | nDList.push(transformNDList(item, columnType?.valueType as DuckDBListType | DuckDBArrayType | undefined)); 173 | } 174 | 175 | return nDList; 176 | }; 177 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import duckdb from 'duckdb'; 2 | import type { Factory } from './pool-ts/types.ts'; 3 | import { RecyclingPool } from './recycling-pool.ts'; 4 | import { DefaultSQLTemplate } from './sql-template-default.ts'; 5 | import type { Identifier, Raw, SQLParamType, Values } from './sql-template.ts'; 6 | import { SQLDefault, SQLIdentifier, SQLRaw, SQLValues } from './sql-template.ts'; 7 | 8 | export { SQLTemplate } from './sql-template.ts'; 9 | export interface SQL { 10 | (strings: TemplateStringsArray, ...params: SQLParamType[]): DefaultSQLTemplate; 11 | identifier(value: Identifier): SQLIdentifier; 12 | values(value: Values): SQLValues; 13 | raw(value: Raw): SQLRaw; 14 | default: SQLDefault; 15 | } 16 | 17 | const createSqlTemplate = (pool: RecyclingPool): SQL => { 18 | // [strings, params]: Parameters 19 | const fn = (strings: TemplateStringsArray, ...params: SQLParamType[]): DefaultSQLTemplate => { 20 | return new DefaultSQLTemplate(strings, params, pool); 21 | }; 22 | 23 | Object.assign(fn, { 24 | identifier: (value: Identifier) => { 25 | return new SQLIdentifier(value); 26 | }, 27 | values: (value: Values) => { 28 | return new SQLValues(value); 29 | }, 30 | raw: (value: Raw) => { 31 | return new SQLRaw(value); 32 | }, 33 | default: new SQLDefault(), 34 | }); 35 | 36 | return fn as any; 37 | }; 38 | 39 | function createDatabase(url: string, options: Record): Promise { 40 | return new Promise((resolve, reject) => { 41 | const db = new duckdb.Database(url, options, (err) => { 42 | if (err) { 43 | return reject(err); 44 | } 45 | resolve(db); 46 | }); 47 | }); 48 | } 49 | 50 | const createFactory = ( 51 | { 52 | url, 53 | accessMode = 'read_write', 54 | maxMemory = '512MB', 55 | threads = '4', 56 | }: { 57 | url: string; 58 | accessMode?: 'read_only' | 'read_write'; 59 | maxMemory?: string; 60 | threads?: string; 61 | }, 62 | ) => { 63 | const factory: Factory = { 64 | create: async function() { 65 | // wrapping duckdb driver error in new js error to add stack trace to it 66 | try { 67 | const db = await createDatabase(url, { 68 | access_mode: accessMode, 69 | max_memory: maxMemory, 70 | threads: threads, 71 | }); 72 | 73 | // Run any connection initialization commands here 74 | 75 | return db; 76 | } catch (error) { 77 | const newError = new Error((error as Error).message); 78 | throw newError; 79 | } 80 | }, 81 | destroy: async function(db: duckdb.Database) { 82 | // wrapping duckdb driver error in js error to add stack trace to it 83 | try { 84 | return db.close(); 85 | } catch (error) { 86 | const newError = new Error((error as Error).message); 87 | throw newError; 88 | } 89 | }, 90 | }; 91 | 92 | return factory; 93 | }; 94 | 95 | export function waddler( 96 | { 97 | dbUrl, 98 | url, 99 | min = 1, 100 | max = 1, 101 | accessMode = 'read_write', 102 | maxMemory = '512MB', 103 | threads = '4', 104 | }: { 105 | /** @deprecated */ 106 | dbUrl?: string; 107 | url: string; 108 | min?: number; 109 | max?: number; 110 | accessMode?: 'read_only' | 'read_write'; 111 | maxMemory?: string; 112 | threads?: string; 113 | }, 114 | ) { 115 | url = url === undefined && dbUrl !== undefined ? dbUrl : url; 116 | 117 | const factory = createFactory({ 118 | url, 119 | accessMode, 120 | maxMemory, 121 | threads, 122 | }); 123 | const options = { 124 | max, // maximum size of the pool 125 | min, // minimum size of the pool 126 | }; 127 | 128 | const pool = new RecyclingPool(factory, options); 129 | 130 | return createSqlTemplate(pool); 131 | } 132 | -------------------------------------------------------------------------------- /src/neo.ts: -------------------------------------------------------------------------------- 1 | import type { DuckDBResult } from '@duckdb/node-api'; 2 | import { DuckDBInstance } from '@duckdb/node-api'; 3 | import { transformResultToArrays, transformResultToObjects } from './duckdb-neo/result-transformers.ts'; 4 | import { bindParams } from './duckdb-neo/utils.ts'; 5 | import type { Factory } from './pool-ts/types.ts'; 6 | import { RecyclingPool } from './recycling-pool.ts'; 7 | import { NeoSQLTemplate } from './sql-template-neo.ts'; 8 | import type { Identifier, Raw, SQLParamType, Values } from './sql-template.ts'; 9 | import { SQLDefault, SQLIdentifier, SQLRaw, SQLValues } from './sql-template.ts'; 10 | import type { DuckDBConnectionObj, UnsafeParamType } from './types.ts'; 11 | 12 | type RowData = { 13 | [columnName: string]: any; 14 | }; 15 | 16 | export { SQLTemplate } from './sql-template.ts'; 17 | export interface SQL { 18 | (strings: TemplateStringsArray, ...params: SQLParamType[]): NeoSQLTemplate; 19 | identifier(value: Identifier): SQLIdentifier; 20 | values(value: Values): SQLValues; 21 | raw(value: Raw): SQLRaw; 22 | unsafe(query: string, params?: UnsafeParamType[], options?: { rowMode: 'array' | 'default' }): Promise< 23 | { 24 | [columnName: string]: any; 25 | }[] | any[][] 26 | >; 27 | default: SQLDefault; 28 | } 29 | 30 | const createSqlTemplate = (pool: RecyclingPool): SQL => { 31 | // [strings, params]: Parameters 32 | const fn = (strings: TemplateStringsArray, ...params: SQLParamType[]): NeoSQLTemplate => { 33 | return new NeoSQLTemplate(strings, params, pool); 34 | }; 35 | 36 | Object.assign(fn, { 37 | identifier: (value: Identifier) => { 38 | return new SQLIdentifier(value); 39 | }, 40 | values: (value: Values) => { 41 | return new SQLValues(value); 42 | }, 43 | raw: (value: Raw) => { 44 | return new SQLRaw(value); 45 | }, 46 | unsafe: async (query: string, params?: UnsafeParamType[], options?: { rowMode: 'array' | 'default' }) => { 47 | return await unsafeFunc(pool, query, params, options); 48 | }, 49 | default: new SQLDefault(), 50 | }); 51 | 52 | return fn as any; 53 | }; 54 | 55 | const unsafeFunc = async ( 56 | pool: RecyclingPool, 57 | query: string, 58 | params?: UnsafeParamType[], 59 | options?: { rowMode: 'array' | 'default' }, 60 | ) => { 61 | params = params ?? []; 62 | const rowMode = options?.rowMode ?? 'default'; 63 | const connObj = await pool.acquire(); 64 | 65 | let duckDbResult: DuckDBResult; 66 | 67 | // wrapping duckdb driver error in new js error to add stack trace to it 68 | try { 69 | const prepared = await connObj.connection.prepare(query); 70 | bindParams(prepared, params); 71 | 72 | duckDbResult = await prepared.run(); 73 | } catch (error) { 74 | await pool.release(connObj); 75 | const newError = new Error((error as Error).message); 76 | throw newError; 77 | } 78 | 79 | if (rowMode === 'default') { 80 | const result = await transformResultToObjects(duckDbResult); 81 | 82 | await pool.release(connObj); 83 | 84 | return result; 85 | } 86 | 87 | // rowMode === "array" 88 | const result = await transformResultToArrays(duckDbResult); 89 | 90 | await pool.release(connObj); 91 | 92 | return result; 93 | }; 94 | 95 | const createFactory = ( 96 | { 97 | url, 98 | accessMode = 'read_write', 99 | maxMemory = '512MB', 100 | threads = '4', 101 | }: { 102 | url: string; 103 | accessMode?: 'read_only' | 'read_write'; 104 | maxMemory?: string; 105 | threads?: string; 106 | }, 107 | ) => { 108 | const factory: Factory = { 109 | create: async function() { 110 | // wrapping duckdb driver error in new js error to add stack trace to it 111 | try { 112 | const instance = await DuckDBInstance.create(url, { 113 | access_mode: accessMode, 114 | max_memory: maxMemory, 115 | threads: threads, 116 | }); 117 | const conn = await instance.connect(); 118 | 119 | const connObj: DuckDBConnectionObj = { instance, connection: conn }; 120 | // Run any connection initialization commands here 121 | 122 | return connObj; 123 | } catch (error) { 124 | const newError = new Error((error as Error).message); 125 | throw newError; 126 | } 127 | }, 128 | destroy: async function() {}, 129 | }; 130 | 131 | return factory; 132 | }; 133 | 134 | export function waddler( 135 | { 136 | dbUrl, 137 | url, 138 | min = 1, 139 | max = 1, 140 | accessMode = 'read_write', 141 | maxMemory = '512MB', 142 | threads = '4', 143 | }: { 144 | /** @deprecated */ 145 | dbUrl?: string; 146 | url: string; 147 | min?: number; 148 | max?: number; 149 | accessMode?: 'read_only' | 'read_write'; 150 | maxMemory?: string; 151 | threads?: string; 152 | }, 153 | ) { 154 | url = url === undefined && dbUrl !== undefined ? dbUrl : url; 155 | 156 | const factory = createFactory({ 157 | url, 158 | accessMode, 159 | maxMemory, 160 | threads, 161 | }); 162 | const options = { 163 | max, // maximum size of the pool 164 | min, // minimum size of the pool 165 | }; 166 | 167 | const pool = new RecyclingPool(factory, options); 168 | 169 | return createSqlTemplate(pool); 170 | } 171 | -------------------------------------------------------------------------------- /src/pool-ts/DefaultEvictor.ts: -------------------------------------------------------------------------------- 1 | import type { PooledResource } from './PooledResource.ts'; 2 | import type { IEvictorConfig } from './types.ts'; 3 | 4 | export class DefaultEvictor { 5 | evict(config: IEvictorConfig, pooledResource: PooledResource, availableObjectsCount: number) { 6 | const idleTime = Date.now() - pooledResource.lastIdleTime!; 7 | 8 | if ( 9 | config.softIdleTimeoutMillis > 0 10 | && config.softIdleTimeoutMillis < idleTime 11 | && config.min < availableObjectsCount 12 | ) { 13 | return true; 14 | } 15 | 16 | if (config.idleTimeoutMillis < idleTime) { 17 | return true; 18 | } 19 | 20 | return false; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/pool-ts/Deferred.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is apparently a bit like a Jquery deferred, hence the name 3 | */ 4 | 5 | export class Deferred { 6 | static readonly PENDING = 'PENDING'; 7 | static readonly FULFILLED = 'FULFILLED'; 8 | static readonly REJECTED = 'REJECTED'; 9 | 10 | protected _state: 'PENDING' | 'FULFILLED' | 'REJECTED'; 11 | private _resolve: ((value: T | PromiseLike) => void) | undefined; 12 | private _reject: ((reason?: any) => void) | undefined; 13 | private _promise: Promise; 14 | 15 | constructor(promiseConstructor: PromiseConstructor) { 16 | this._state = Deferred.PENDING; 17 | 18 | this._promise = new promiseConstructor((resolve, reject) => { 19 | this._resolve = resolve; 20 | this._reject = reject; 21 | }); 22 | } 23 | 24 | get state() { 25 | return this._state; 26 | } 27 | 28 | get promise() { 29 | return this._promise; 30 | } 31 | 32 | reject(reason: any) { 33 | if (this._state !== Deferred.PENDING) { 34 | return; 35 | } 36 | this._state = Deferred.REJECTED; 37 | this._reject!(reason); 38 | } 39 | 40 | resolve(value: T) { 41 | if (this._state !== Deferred.PENDING) { 42 | return; 43 | } 44 | this._state = Deferred.FULFILLED; 45 | this._resolve!(value); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/pool-ts/Deque.ts: -------------------------------------------------------------------------------- 1 | import { DequeIterator } from './DequeIterator.ts'; 2 | import { DoublyLinkedList, Node } from './DoublyLinkedList.ts'; 3 | 4 | /** 5 | * DoublyLinkedList backed double ended queue 6 | * implements just enough to keep the Pool 7 | */ 8 | export class Deque { 9 | protected _list: DoublyLinkedList; 10 | constructor() { 11 | this._list = new DoublyLinkedList(); 12 | } 13 | 14 | /** 15 | * removes and returns the first element from the queue 16 | * @return [description] 17 | */ 18 | shift() { 19 | if (this.length <= 0) { 20 | return null; 21 | } 22 | 23 | const node = this._list.head!; 24 | this._list.remove(node); 25 | 26 | return node.data; 27 | } 28 | 29 | /** 30 | * adds one elemts to the beginning of the queue 31 | * @param {any} element [description] 32 | * @return [description] 33 | */ 34 | unshift(element: T) { 35 | const node = new Node(element); 36 | 37 | this._list.insertBeginning(node); 38 | } 39 | 40 | /** 41 | * adds one to the end of the queue 42 | * @param {any} element [description] 43 | * @return [description] 44 | */ 45 | push(element: T) { 46 | const node = new Node(element); 47 | 48 | this._list.insertEnd(node); 49 | } 50 | 51 | /** 52 | * removes and returns the last element from the queue 53 | */ 54 | pop() { 55 | if (this.length <= 0) { 56 | return null; 57 | } 58 | 59 | const node = this._list.tail!; 60 | this._list.remove(node); 61 | 62 | return node.data; 63 | } 64 | 65 | [Symbol.iterator]() { 66 | return new DequeIterator(this._list); 67 | } 68 | 69 | iterator() { 70 | return new DequeIterator(this._list); 71 | } 72 | 73 | reverseIterator() { 74 | return new DequeIterator(this._list, true); 75 | } 76 | 77 | /** 78 | * get a reference to the item at the head of the queue 79 | * @return [description] 80 | */ 81 | get head() { 82 | if (this.length === 0) { 83 | return null; 84 | } 85 | const node = this._list.head!; 86 | return node.data; 87 | } 88 | 89 | /** 90 | * get a reference to the item at the tail of the queue 91 | * @return [description] 92 | */ 93 | get tail() { 94 | if (this.length === 0) { 95 | return null; 96 | } 97 | const node = this._list.tail!; 98 | return node.data; 99 | } 100 | 101 | get length() { 102 | return this._list.length; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/pool-ts/DequeIterator.ts: -------------------------------------------------------------------------------- 1 | import type { DoublyLinkedList, Node } from './DoublyLinkedList.ts'; 2 | 3 | /** 4 | * Thin wrapper around an underlying DDL iterator 5 | */ 6 | export class DequeIterator implements Iterator { 7 | private _list: DoublyLinkedList; 8 | private _direction: 'prev' | 'next'; 9 | private _startPosition: 'tail' | 'head'; 10 | private _started: boolean; 11 | private _cursor: Node | null; 12 | private _done: boolean; 13 | /** 14 | * @param {Object} deque a node that is part of a doublyLinkedList 15 | * @param {Boolean} [reverse=false] is this a reverse iterator? default: false 16 | */ 17 | constructor(dll: DoublyLinkedList, reverse: boolean = false) { 18 | this._list = dll; 19 | // NOTE: these key names are tied to the DoublyLinkedListIterator 20 | this._direction = reverse === true ? 'prev' : 'next'; 21 | this._startPosition = reverse === true ? 'tail' : 'head'; 22 | this._started = false; 23 | this._cursor = null; 24 | this._done = false; 25 | } 26 | 27 | _start() { 28 | this._cursor = this._list[this._startPosition]; 29 | this._started = true; 30 | } 31 | 32 | _advanceCursor() { 33 | if (this._started === false) { 34 | this._start(); 35 | return; 36 | } 37 | this._cursor = this._cursor![this._direction]; 38 | } 39 | 40 | reset() { 41 | this._done = false; 42 | this._started = false; 43 | this._cursor = null; 44 | } 45 | 46 | remove() { 47 | if ( 48 | this._started === false 49 | || this._done === true 50 | || this._isCursorDetached() 51 | ) { 52 | return false; 53 | } 54 | this._list.remove(this._cursor!); 55 | 56 | // TODO: revise 57 | return; 58 | } 59 | 60 | next(): { done: true; value: undefined } | { value: T; done?: false } { 61 | if (this._done === true) { 62 | return { done: true, value: undefined }; 63 | } 64 | 65 | this._advanceCursor(); 66 | 67 | // if there is no node at the cursor or the node at the cursor is no longer part of 68 | // a doubly linked list then we are done/finished/kaput 69 | if (this._cursor === null || this._isCursorDetached()) { 70 | this._done = true; 71 | return { done: true, value: undefined }; 72 | } 73 | 74 | return { 75 | value: this._cursor.data, 76 | done: false, 77 | }; 78 | } 79 | 80 | /** 81 | * Is the node detached from a list? 82 | * NOTE: you can trick/bypass/confuse this check by removing a node from one DoublyLinkedList 83 | * and adding it to another. 84 | * TODO: We can make this smarter by checking the direction of travel and only checking 85 | * the required next/prev/head/tail rather than all of them 86 | * @return {Boolean} [description] 87 | */ 88 | _isCursorDetached() { 89 | return ( 90 | this._cursor!.prev === null 91 | && this._cursor!.next === null 92 | && this._list.tail !== this._cursor 93 | && this._list.head !== this._cursor 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/pool-ts/DoublyLinkedList.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A Doubly Linked List, because there aren't enough in the world... 3 | * this is pretty much a direct JS port of the one wikipedia 4 | * https://en.wikipedia.org/wiki/Doubly_linked_list 5 | * 6 | * For most usage 'insertBeginning' and 'insertEnd' should be enough 7 | * 8 | * nodes are expected to something like a POJSO like 9 | * { 10 | * prev: null, 11 | * next: null, 12 | * something: 'whatever you like' 13 | * } 14 | */ 15 | 16 | export class Node { 17 | prev: Node | null = null; 18 | next: Node | null = null; 19 | constructor(public data: T) {} 20 | } 21 | 22 | export class DoublyLinkedList { 23 | head: Node | null; 24 | tail: Node | null; 25 | length: number; 26 | 27 | constructor() { 28 | this.head = null; 29 | this.tail = null; 30 | this.length = 0; 31 | } 32 | 33 | insertBeginning(node: Node) { 34 | // DoublyLinkedList is empty 35 | if (this.head === null) { 36 | this.head = node; 37 | this.tail = node; 38 | node.prev = null; 39 | node.next = null; 40 | this.length++; 41 | } else { 42 | this.insertBefore(this.head, node); 43 | } 44 | } 45 | 46 | insertEnd(node: Node) { 47 | // DoublyLinkedList is empty 48 | if (this.tail === null) { 49 | this.insertBeginning(node); 50 | } else { 51 | this.insertAfter(this.tail, node); 52 | } 53 | } 54 | 55 | insertAfter(node: Node, newNode: Node) { 56 | newNode.prev = node; 57 | newNode.next = node.next; 58 | 59 | if (node.next === null) { 60 | this.tail = newNode; 61 | } else { 62 | node.next.prev = newNode; 63 | } 64 | 65 | node.next = newNode; 66 | this.length++; 67 | } 68 | 69 | insertBefore(node: Node, newNode: Node) { 70 | newNode.prev = node.prev; 71 | newNode.next = node; 72 | 73 | if (node.prev === null) { 74 | this.head = newNode; 75 | } else { 76 | node.prev.next = newNode; 77 | } 78 | 79 | node.prev = newNode; 80 | this.length++; 81 | } 82 | 83 | remove(node: Node) { 84 | if (node.prev === null) { 85 | this.head = node.next; 86 | } else { 87 | node.prev.next = node.next; 88 | } 89 | 90 | if (node.next === null) { 91 | this.tail = node.prev; 92 | } else { 93 | node.next.prev = node.prev; 94 | } 95 | 96 | node.prev = null; 97 | node.next = null; 98 | this.length--; 99 | } 100 | 101 | // FIXME: this should not live here and has become a dumping ground... 102 | static createNode(data: any) { 103 | return new Node(data); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/pool-ts/DoublyLinkedListIterator.ts: -------------------------------------------------------------------------------- 1 | import type { DoublyLinkedList, Node } from './DoublyLinkedList'; 2 | 3 | /** 4 | * Creates an interator for a DoublyLinkedList starting at the given node 5 | * It's internal cursor will remains relative to the last "iterated" node as that 6 | * node moves through the list until it either iterates to the end of the list, 7 | * or the the node it's tracking is removed from the list. Until the first 'next' 8 | * call it tracks the head/tail of the linked list. This means that one can create 9 | * an iterator on an empty list, then add nodes, and then the iterator will follow 10 | * those nodes. Because the DoublyLinkedList nodes don't track their owning "list" and 11 | * it's highly inefficient to walk the list for every iteration, the iterator won't know 12 | * if the node has been detached from one List and added to another list, or if the iterator 13 | * 14 | * The created object is an es6 compatible iterator 15 | */ 16 | export class DoublyLinkedListIterator { 17 | private _list: DoublyLinkedList; 18 | private _direction: 'prev' | 'next'; 19 | private _startPosition: 'tail' | 'head'; 20 | private _started: boolean; 21 | private _cursor: Node | null; 22 | private _done: boolean; 23 | /** 24 | * @param {Object} doublyLinkedList a node that is part of a doublyLinkedList 25 | * @param {Boolean} [reverse=false] is this a reverse iterator? default: false 26 | */ 27 | constructor(doublyLinkedList: DoublyLinkedList, reverse: boolean = false) { 28 | this._list = doublyLinkedList; 29 | // NOTE: these key names are tied to the DoublyLinkedListIterator 30 | this._direction = reverse === true ? 'prev' : 'next'; 31 | this._startPosition = reverse === true ? 'tail' : 'head'; 32 | this._started = false; 33 | this._cursor = null; 34 | this._done = false; 35 | } 36 | 37 | _start() { 38 | this._cursor = this._list[this._startPosition]; 39 | this._started = true; 40 | } 41 | 42 | _advanceCursor() { 43 | if (this._started === false) { 44 | this._start(); 45 | return; 46 | } 47 | this._cursor = this._cursor![this._direction]; 48 | } 49 | 50 | reset() { 51 | this._done = false; 52 | this._started = false; 53 | this._cursor = null; 54 | } 55 | 56 | remove() { 57 | if ( 58 | this._started === false 59 | || this._done === true 60 | || this._isCursorDetached() 61 | ) { 62 | return false; 63 | } 64 | this._list.remove(this._cursor!); 65 | // TODO: revise 66 | return; 67 | } 68 | 69 | next() { 70 | if (this._done === true) { 71 | return { done: true }; 72 | } 73 | 74 | this._advanceCursor(); 75 | 76 | // if there is no node at the cursor or the node at the cursor is no longer part of 77 | // a doubly linked list then we are done/finished/kaput 78 | if (this._cursor === null || this._isCursorDetached()) { 79 | this._done = true; 80 | return { done: true }; 81 | } 82 | 83 | return { 84 | value: this._cursor, 85 | done: false, 86 | }; 87 | } 88 | 89 | /** 90 | * Is the node detached from a list? 91 | * NOTE: you can trick/bypass/confuse this check by removing a node from one DoublyLinkedList 92 | * and adding it to another. 93 | * TODO: We can make this smarter by checking the direction of travel and only checking 94 | * the required next/prev/head/tail rather than all of them 95 | * @return {Boolean} [description] 96 | */ 97 | _isCursorDetached(): boolean { 98 | return ( 99 | this._cursor!.prev === null 100 | && this._cursor!.next === null 101 | && this._list.tail !== this._cursor 102 | && this._list.head !== this._cursor 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/pool-ts/Pool.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import type { DefaultEvictor } from './DefaultEvictor.ts'; 4 | import { Deferred } from './Deferred.ts'; 5 | import type { Deque } from './Deque.ts'; 6 | import type { DequeIterator } from './DequeIterator.ts'; 7 | import { validateFactory as factoryValidator } from './factoryValidator.ts'; 8 | import { PooledResource } from './PooledResource.ts'; 9 | import { PoolOptions } from './PoolOptions.ts'; 10 | import type { PriorityQueue } from './PriorityQueue.ts'; 11 | import { ResourceLoan } from './ResourceLoan.ts'; 12 | import { ResourceRequest } from './ResourceRequest.ts'; 13 | import type { Factory, Options } from './types.ts'; 14 | import { reflector } from './utils.ts'; 15 | 16 | export class Pool { 17 | private _config: PoolOptions; 18 | private promiseConstructor: typeof Promise; 19 | private _factory: Factory; 20 | private _draining: boolean; 21 | private _started: boolean; 22 | private _waitingClientsQueue: PriorityQueue; 23 | private _factoryCreateOperations: Set>; 24 | private _factoryDestroyOperations: Set>; 25 | private _availableObjects: Deque>; 26 | private _testOnBorrowResources: Set>; 27 | private _testOnReturnResources: Set>; 28 | private _validationOperations: Set>; 29 | private _allObjects: Set>; 30 | protected _resourceLoans: Map>; 31 | private _evictionIterator: DequeIterator>; 32 | private _evictor: DefaultEvictor; 33 | private _scheduledEviction: NodeJS.Timeout | null; 34 | 35 | constructor( 36 | Evictor: typeof DefaultEvictor, 37 | dequeConstructor: typeof Deque, 38 | priorityQueueConstructor: typeof PriorityQueue, 39 | factory: Factory, 40 | options?: Options, 41 | ) { 42 | factoryValidator(factory); 43 | 44 | this._config = new PoolOptions(options); 45 | this.promiseConstructor = this._config.promiseConstructor; 46 | 47 | this._factory = factory; 48 | this._draining = false; 49 | this._started = false; 50 | this._waitingClientsQueue = new priorityQueueConstructor(this._config.priorityRange); 51 | this._factoryCreateOperations = new Set(); 52 | this._factoryDestroyOperations = new Set(); 53 | this._availableObjects = new dequeConstructor(); 54 | this._testOnBorrowResources = new Set(); 55 | this._testOnReturnResources = new Set(); 56 | this._validationOperations = new Set(); 57 | this._allObjects = new Set(); 58 | this._resourceLoans = new Map(); 59 | this._evictionIterator = this._availableObjects.iterator(); 60 | this._evictor = new Evictor(); 61 | this._scheduledEviction = null; 62 | } 63 | 64 | private async _destroy(pooledResource: PooledResource) { 65 | pooledResource.invalidate(); 66 | this._allObjects.delete(pooledResource); 67 | await this._factory.destroy(pooledResource.obj); 68 | 69 | await this._ensureMinimum(); 70 | } 71 | 72 | // not in use for now 73 | private _applyDestroyTimeout(promise: Promise) { 74 | const timeoutPromise = new this.promiseConstructor((resolve, reject) => { 75 | setTimeout(() => { 76 | reject(new Error('destroy timed out')); 77 | }, this._config.destroyTimeoutMillis!).unref(); 78 | }); 79 | return this.promiseConstructor.race([timeoutPromise, promise]); 80 | } 81 | 82 | // not in use because this._config.testOnBorrow default equals false 83 | private _testOnBorrow(): boolean { 84 | if (this._availableObjects.length < 1) { 85 | return false; 86 | } 87 | 88 | const pooledResource = this._availableObjects.shift()!; 89 | pooledResource.test(); 90 | this._testOnBorrowResources.add(pooledResource); 91 | 92 | const validationPromise = (this._factory.validate === undefined) 93 | ? Promise.resolve(true) 94 | : this._factory.validate(pooledResource.obj); 95 | 96 | const wrappedValidationPromise = this.promiseConstructor.resolve(validationPromise); 97 | 98 | this._trackOperation( 99 | wrappedValidationPromise, 100 | this._validationOperations, 101 | ).then((isValid) => { 102 | this._testOnBorrowResources.delete(pooledResource); 103 | 104 | if (!isValid) { 105 | pooledResource.invalidate(); 106 | this._destroy(pooledResource); 107 | this._dispense(); 108 | return; 109 | } 110 | this._dispatchPooledResourceToNextWaitingClient(pooledResource); 111 | }); 112 | 113 | return true; 114 | } 115 | 116 | private _dispatchResource(): boolean { 117 | if (this._availableObjects.length < 1) { 118 | return false; 119 | } 120 | 121 | const pooledResource = this._availableObjects.shift()!; 122 | return this._dispatchPooledResourceToNextWaitingClient(pooledResource); 123 | } 124 | 125 | private async _dispense() { 126 | const numWaitingClients = this._waitingClientsQueue.length; 127 | 128 | if (numWaitingClients < 1) { 129 | return; 130 | } 131 | 132 | const resourceShortfall = numWaitingClients - this._potentiallyAllocableResourceCount; 133 | 134 | const actualNumberOfResourcesToCreate = Math.min( 135 | this.spareResourceCapacity, 136 | resourceShortfall, 137 | ); 138 | 139 | const resourceCreationPromiseList = []; 140 | for (let i = 0; actualNumberOfResourcesToCreate > i; i++) { 141 | resourceCreationPromiseList.push(this._createResource()); 142 | } 143 | await Promise.all(resourceCreationPromiseList); 144 | 145 | // this._config.testOnBorrow default equals false 146 | if (this._config.testOnBorrow) { 147 | const desiredNumberOfResourcesToMoveIntoTest = numWaitingClients - this._testOnBorrowResources.size; 148 | const actualNumberOfResourcesToMoveIntoTest = Math.min( 149 | this._availableObjects.length, 150 | desiredNumberOfResourcesToMoveIntoTest, 151 | ); 152 | for (let i = 0; actualNumberOfResourcesToMoveIntoTest > i; i++) { 153 | this._testOnBorrow(); 154 | } 155 | } 156 | 157 | if (!this._config.testOnBorrow) { 158 | const actualNumberOfResourcesToDispatch = Math.min( 159 | this._availableObjects.length, 160 | numWaitingClients, 161 | ); 162 | for (let i = 0; actualNumberOfResourcesToDispatch > i; i++) { 163 | this._dispatchResource(); 164 | } 165 | } 166 | } 167 | 168 | private _dispatchPooledResourceToNextWaitingClient( 169 | pooledResource: PooledResource, 170 | ): boolean { 171 | // TODO: i might need to iterate over the waitingClientsQueue using while loop skipping the non-pending clients 172 | const clientResourceRequest = this._waitingClientsQueue.dequeue(); 173 | if ( 174 | clientResourceRequest === null 175 | || clientResourceRequest.state !== Deferred.PENDING 176 | ) { 177 | this._addPooledResourceToAvailableObjects(pooledResource); 178 | return false; 179 | } 180 | const loan = new ResourceLoan(pooledResource, this.promiseConstructor); 181 | this._resourceLoans.set(pooledResource.obj, loan); 182 | pooledResource.allocate(); 183 | 184 | clientResourceRequest.resolve(pooledResource.obj); 185 | return true; 186 | } 187 | 188 | // not in use for now 189 | private _trackOperation( 190 | operation: Promise, 191 | set: Set>, 192 | ): Promise { 193 | set.add(operation); 194 | 195 | return operation.then( 196 | (v) => { 197 | set.delete(operation); 198 | return this.promiseConstructor.resolve(v); 199 | }, 200 | (e) => { 201 | set.delete(operation); 202 | return this.promiseConstructor.reject(e); 203 | }, 204 | ); 205 | } 206 | 207 | private async _createResource() { 208 | const factoryPromise = this._factory.create(); 209 | try { 210 | this._factoryCreateOperations.add(factoryPromise); 211 | 212 | const resource: T = await factoryPromise; 213 | const pooledResource = new PooledResource(resource); 214 | this._allObjects.add(pooledResource); 215 | this._addPooledResourceToAvailableObjects(pooledResource); 216 | 217 | this._factoryCreateOperations.delete(factoryPromise); 218 | await this._dispense(); 219 | } catch (error) { 220 | await this._dispense(); 221 | this._factoryCreateOperations.delete(factoryPromise); 222 | 223 | throw error; 224 | } 225 | } 226 | 227 | private async _ensureMinimum() { 228 | if (this._draining === true) { 229 | return; 230 | } 231 | 232 | const resourceCreationPromiseList = []; 233 | const minShortfall = this._config.min - this._count; 234 | for (let i = 0; i < minShortfall; i++) { 235 | resourceCreationPromiseList.push(this._createResource()); 236 | } 237 | 238 | await Promise.all(resourceCreationPromiseList); 239 | } 240 | 241 | // not in use for now 242 | private _evict(): void { 243 | const testsToRun = Math.min( 244 | this._config.numTestsPerEvictionRun, 245 | this._availableObjects.length, 246 | ); 247 | const evictionConfig = { 248 | softIdleTimeoutMillis: this._config.softIdleTimeoutMillis, 249 | idleTimeoutMillis: this._config.idleTimeoutMillis, 250 | min: this._config.min, 251 | }; 252 | for (let testsHaveRun = 0; testsHaveRun < testsToRun;) { 253 | const iterationResult = this._evictionIterator.next(); 254 | 255 | if (iterationResult.done === true && this._availableObjects.length < 1) { 256 | this._evictionIterator.reset(); 257 | return; 258 | } 259 | if (iterationResult.done === true && this._availableObjects.length > 0) { 260 | this._evictionIterator.reset(); 261 | continue; 262 | } 263 | 264 | const resource = iterationResult.value!; 265 | 266 | const shouldEvict = this._evictor.evict( 267 | evictionConfig, 268 | resource, 269 | this._availableObjects.length, 270 | ); 271 | testsHaveRun++; 272 | 273 | if (shouldEvict === true) { 274 | this._evictionIterator.remove(); 275 | this._destroy(resource); 276 | } 277 | } 278 | } 279 | 280 | // not in use because this._config.evictionRunIntervalMillis default equals 0 281 | private _scheduleEvictorRun(): void { 282 | // this._config.evictionRunIntervalMillis default equals 0 283 | if (this._config.evictionRunIntervalMillis > 0) { 284 | this._scheduledEviction = setTimeout(() => { 285 | this._evict(); 286 | this._scheduleEvictorRun(); 287 | }, this._config.evictionRunIntervalMillis).unref(); 288 | } 289 | } 290 | 291 | // not in use for now 292 | private _descheduleEvictorRun(): void { 293 | if (this._scheduledEviction) { 294 | clearTimeout(this._scheduledEviction); 295 | } 296 | this._scheduledEviction = null; 297 | } 298 | 299 | protected async start() { 300 | if (this._draining === true || this._started === true) return; 301 | 302 | this._started = true; 303 | try { 304 | this._scheduleEvictorRun(); 305 | await this._ensureMinimum(); 306 | } catch (error) { 307 | this._started = false; 308 | throw error; 309 | } 310 | } 311 | 312 | async acquire(priority?: number) { 313 | if (this._started === false) { 314 | await this.start(); 315 | } 316 | 317 | if (this._draining) { 318 | throw new Error('pool is draining and cannot accept work'); 319 | } 320 | 321 | if ( 322 | this.spareResourceCapacity < 1 323 | && this._availableObjects.length < 1 324 | && this._config.maxWaitingClients !== undefined 325 | && this._waitingClientsQueue.length >= this._config.maxWaitingClients 326 | ) { 327 | throw new Error('max waitingClients count exceeded'); 328 | } 329 | 330 | const resourceRequest = new ResourceRequest( 331 | this._config.acquireTimeoutMillis, 332 | this.promiseConstructor, 333 | ); 334 | this._waitingClientsQueue.enqueue(resourceRequest, priority); 335 | try { 336 | await this._dispense(); 337 | } catch (error) { 338 | if (resourceRequest.state === Deferred.PENDING) { 339 | resourceRequest.reject(error); 340 | } 341 | throw error; 342 | } 343 | 344 | return resourceRequest.promise; 345 | } 346 | 347 | // not in use for now 348 | async use(fn: (resource: any) => Promise, priority?: number): Promise { 349 | const resource_1 = await this.acquire(priority); 350 | return await fn(resource_1).then( 351 | (result) => { 352 | this.release(resource_1); 353 | return result; 354 | }, 355 | (err) => { 356 | this.destroy(resource_1); 357 | throw err; 358 | }, 359 | ); 360 | } 361 | 362 | // not in use for now 363 | isBorrowedResource(resource: T): boolean { 364 | return this._resourceLoans.has(resource); 365 | } 366 | 367 | async release(resource: T): Promise { 368 | const loan = this._resourceLoans.get(resource); 369 | 370 | if (loan === undefined) { 371 | throw new Error('Resource not currently part of this pool'); 372 | } 373 | 374 | this._resourceLoans.delete(resource); 375 | 376 | // TODO: revise(not sure if this line is doing anything) 377 | loan.resolve(resource); 378 | 379 | const pooledResource = loan.pooledResource; 380 | 381 | pooledResource.deallocate(); 382 | this._addPooledResourceToAvailableObjects(pooledResource); 383 | 384 | await this._dispense(); 385 | } 386 | 387 | async destroy(resource: T): Promise { 388 | const loan = this._resourceLoans.get(resource); 389 | 390 | if (loan === undefined) { 391 | return this.promiseConstructor.reject( 392 | new Error('Resource not currently part of this pool'), 393 | ); 394 | } 395 | 396 | this._resourceLoans.delete(resource); 397 | 398 | // TODO: revise(not sure if this line is doing anything) 399 | loan.resolve(resource); 400 | const pooledResource = loan.pooledResource; 401 | 402 | pooledResource.deallocate(); 403 | if (this._count - 1 >= this.min) { 404 | await this._destroy(pooledResource); 405 | } else { 406 | this._addPooledResourceToAvailableObjects(pooledResource); 407 | } 408 | 409 | await this._dispense(); 410 | } 411 | 412 | private _addPooledResourceToAvailableObjects(pooledResource: PooledResource): void { 413 | pooledResource.idle(); 414 | if (this._config.fifo === true) { 415 | this._availableObjects.push(pooledResource); 416 | } else { 417 | this._availableObjects.unshift(pooledResource); 418 | } 419 | } 420 | 421 | // not in use for now 422 | async drain(): Promise { 423 | this._draining = true; 424 | await this.__allResourceRequestsSettled(); 425 | await this.__allResourcesReturned(); 426 | this._descheduleEvictorRun(); 427 | } 428 | 429 | // not in use for now 430 | private __allResourceRequestsSettled(): Promise { 431 | if (this._waitingClientsQueue.length > 0) { 432 | return reflector(this._waitingClientsQueue.tail!.promise); 433 | } 434 | return this.promiseConstructor.resolve(); 435 | } 436 | 437 | // not in use due to not using drain method 438 | private __allResourcesReturned(): Promise { 439 | const ps = [...this._resourceLoans.values()] 440 | .map((loan) => loan.promise) 441 | .map((element) => reflector(element)); 442 | return this.promiseConstructor.all(ps); 443 | } 444 | 445 | // not in use for now 446 | async clear(): Promise { 447 | const reflectedCreatePromises = [...this._factoryCreateOperations] 448 | .map((element) => reflector(element)); 449 | 450 | await this.promiseConstructor.all(reflectedCreatePromises); 451 | for (const resource of this._availableObjects) { 452 | await this._destroy(resource); 453 | } 454 | const reflectedDestroyPromises = [...this._factoryDestroyOperations] 455 | .map((element_1) => reflector(element_1)); 456 | return await reflector(this.promiseConstructor.all(reflectedDestroyPromises)); 457 | } 458 | 459 | // not in use for now 460 | ready(): Promise { 461 | return new this.promiseConstructor((resolve) => { 462 | const isReady = () => { 463 | if (this.available >= this.min) { 464 | resolve(); 465 | } else { 466 | setTimeout(isReady, 100); 467 | } 468 | }; 469 | 470 | isReady(); 471 | }); 472 | } 473 | 474 | get _potentiallyAllocableResourceCount(): number { 475 | return ( 476 | this._availableObjects.length 477 | + this._testOnBorrowResources.size 478 | + this._testOnReturnResources.size 479 | + this._factoryCreateOperations.size 480 | ); 481 | } 482 | 483 | get _count(): number { 484 | return this._allObjects.size + this._factoryCreateOperations.size; 485 | } 486 | 487 | get spareResourceCapacity(): number { 488 | return ( 489 | this._config.max 490 | - (this._allObjects.size + this._factoryCreateOperations.size) 491 | ); 492 | } 493 | 494 | get size(): number { 495 | return this._count; 496 | } 497 | 498 | get available(): number { 499 | return this._availableObjects.length; 500 | } 501 | 502 | get borrowed(): number { 503 | return this._resourceLoans.size; 504 | } 505 | 506 | get pending(): number { 507 | return this._waitingClientsQueue.length; 508 | } 509 | 510 | get max(): number { 511 | return this._config.max; 512 | } 513 | 514 | get min(): number { 515 | return this._config.min; 516 | } 517 | } 518 | -------------------------------------------------------------------------------- /src/pool-ts/PoolDefaults.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create the default settings used by the pool 3 | * 4 | * @class 5 | */ 6 | export class PoolDefaults { 7 | fifo: boolean; 8 | priorityRange: number; 9 | testOnBorrow: boolean; 10 | testOnReturn: boolean; 11 | evictionRunIntervalMillis: number; 12 | numTestsPerEvictionRun: number; 13 | softIdleTimeoutMillis: number; 14 | idleTimeoutMillis: number; 15 | acquireTimeoutMillis: number | undefined; 16 | destroyTimeoutMillis: number | undefined; 17 | maxWaitingClients: number | undefined; 18 | min: number; 19 | max: number | undefined; 20 | promiseConstructor: PromiseConstructor; 21 | 22 | constructor() { 23 | this.fifo = true; 24 | this.priorityRange = 1; 25 | 26 | this.testOnBorrow = false; 27 | this.testOnReturn = false; 28 | 29 | // setting this.evictionRunIntervalMillis to 0 means this._scheduleEvictorRun will not be executed. 30 | // Therefore, this.softIdleTimeoutMillis and this.idleTimeoutMillis will not have any effect. 31 | this.evictionRunIntervalMillis = 0; 32 | this.numTestsPerEvictionRun = 3; 33 | this.softIdleTimeoutMillis = -1; 34 | this.idleTimeoutMillis = 30000; 35 | 36 | // FIXME: no defaults! 37 | this.acquireTimeoutMillis = undefined; 38 | this.destroyTimeoutMillis = undefined; 39 | this.maxWaitingClients = undefined; 40 | 41 | this.min = 0; 42 | this.max = undefined; 43 | 44 | // FIXME: this seems odd? 45 | this.promiseConstructor = Promise; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/pool-ts/PoolOptions.ts: -------------------------------------------------------------------------------- 1 | import { PoolDefaults } from './PoolDefaults.ts'; 2 | 3 | interface PoolOptionsConfig { 4 | max?: number; 5 | min?: number; 6 | maxWaitingClients?: number; 7 | testOnBorrow?: boolean; 8 | testOnReturn?: boolean; 9 | acquireTimeoutMillis?: number; 10 | destroyTimeoutMillis?: number | null; 11 | priorityRange?: number; 12 | fifo?: boolean; 13 | evictionRunIntervalMillis?: number; 14 | numTestsPerEvictionRun?: number; 15 | softIdleTimeoutMillis?: number; 16 | idleTimeoutMillis?: number; 17 | promiseConstructor?: typeof Promise; 18 | } 19 | 20 | export class PoolOptions { 21 | max: number; 22 | min: number; 23 | maxWaitingClients: number | undefined; 24 | testOnBorrow: boolean; 25 | testOnReturn: boolean; 26 | acquireTimeoutMillis: number | undefined; 27 | destroyTimeoutMillis: number | undefined; 28 | priorityRange: number; 29 | fifo: boolean; 30 | evictionRunIntervalMillis: number; 31 | numTestsPerEvictionRun: number; 32 | softIdleTimeoutMillis: number; 33 | idleTimeoutMillis: number; 34 | promiseConstructor: typeof Promise; 35 | 36 | constructor(opts: PoolOptionsConfig = {}) { 37 | const poolDefaults = new PoolDefaults(); 38 | 39 | this.fifo = opts.fifo ?? poolDefaults.fifo; 40 | 41 | // TODO: check later: opts.priorityRange shouldn't equal 0, therefore using || operator 42 | this.priorityRange = opts.priorityRange || poolDefaults.priorityRange; 43 | 44 | this.testOnBorrow = opts.testOnBorrow ?? poolDefaults.testOnBorrow; 45 | 46 | // TODO: rewrite without casting fields and add assertion check on the fields below 47 | this.testOnReturn = opts.testOnReturn ?? poolDefaults.testOnReturn; 48 | 49 | this.acquireTimeoutMillis = opts.acquireTimeoutMillis === undefined 50 | ? undefined 51 | : Number.parseInt(opts.acquireTimeoutMillis.toString(), 10); 52 | 53 | this.destroyTimeoutMillis = opts.destroyTimeoutMillis 54 | ? Number.parseInt(opts.destroyTimeoutMillis.toString(), 10) 55 | : undefined; 56 | 57 | this.maxWaitingClients = opts.maxWaitingClients === undefined 58 | ? undefined 59 | : Number.parseInt(opts.maxWaitingClients.toString(), 10); 60 | 61 | this.max = Number.parseInt(opts.max?.toString() || '1', 10); 62 | this.min = Number.parseInt(opts.min?.toString() || '1', 10); 63 | 64 | this.max = Math.max(Number.isNaN(this.max) ? 1 : this.max, 1); 65 | this.min = Math.min(Number.isNaN(this.min) ? 1 : this.min, this.max); 66 | 67 | this.evictionRunIntervalMillis = opts.evictionRunIntervalMillis || poolDefaults.evictionRunIntervalMillis; 68 | this.numTestsPerEvictionRun = opts.numTestsPerEvictionRun || poolDefaults.numTestsPerEvictionRun; 69 | this.softIdleTimeoutMillis = opts.softIdleTimeoutMillis || poolDefaults.softIdleTimeoutMillis; 70 | this.idleTimeoutMillis = opts.idleTimeoutMillis || poolDefaults.idleTimeoutMillis; 71 | 72 | this.promiseConstructor = opts.promiseConstructor === undefined 73 | ? poolDefaults.promiseConstructor 74 | : opts.promiseConstructor; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/pool-ts/PooledResource.ts: -------------------------------------------------------------------------------- 1 | enum PooledResourceState { 2 | ALLOCATED = 'ALLOCATED', // In use 3 | IDLE = 'IDLE', // In the queue, not in use. 4 | INVALID = 'INVALID', // Failed validation 5 | RETURNING = 'RETURNING', // Resource is in process of returning 6 | VALIDATION = 'VALIDATION', // Currently being tested 7 | } 8 | 9 | /** 10 | * @class 11 | * @private 12 | */ 13 | export class PooledResource { 14 | creationTime: number; 15 | lastReturnTime: number | null; 16 | lastBorrowTime: number | null; 17 | lastIdleTime: number | null; 18 | obj: T; 19 | state: PooledResourceState; 20 | 21 | constructor(resource: T) { 22 | this.creationTime = Date.now(); 23 | this.lastReturnTime = null; 24 | this.lastBorrowTime = null; 25 | this.lastIdleTime = null; 26 | this.obj = resource; 27 | this.state = PooledResourceState.IDLE; 28 | } 29 | 30 | // mark the resource as "allocated" 31 | allocate(): void { 32 | this.lastBorrowTime = Date.now(); 33 | this.state = PooledResourceState.ALLOCATED; 34 | } 35 | 36 | // mark the resource as "deallocated" 37 | deallocate(): void { 38 | this.lastReturnTime = Date.now(); 39 | this.state = PooledResourceState.IDLE; 40 | } 41 | 42 | invalidate(): void { 43 | this.state = PooledResourceState.INVALID; 44 | } 45 | 46 | test(): void { 47 | this.state = PooledResourceState.VALIDATION; 48 | } 49 | 50 | idle(): void { 51 | this.lastIdleTime = Date.now(); 52 | this.state = PooledResourceState.IDLE; 53 | } 54 | 55 | returning(): void { 56 | this.state = PooledResourceState.RETURNING; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/pool-ts/PriorityQueue.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from './Queue.ts'; 2 | import type { ResourceRequest } from './ResourceRequest.ts'; 3 | 4 | /** 5 | * @class 6 | * @private 7 | */ 8 | export class PriorityQueue { 9 | private _size: number; 10 | private _slots: Queue[]; 11 | 12 | constructor(size: number) { 13 | this._size = Math.max(Math.trunc(+size), 1); 14 | this._slots = []; 15 | 16 | // Initialize arrays to hold queue elements 17 | for (let i = 0; i < this._size; i++) { 18 | this._slots.push(new Queue()); 19 | } 20 | } 21 | 22 | get length(): number { 23 | let _length = 0; 24 | for (let i = 0, sl = this._slots.length; i < sl; i++) { 25 | _length += this._slots[i]!.length; 26 | } 27 | return _length; 28 | } 29 | 30 | enqueue(obj: ResourceRequest, priority?: number): void { 31 | // Convert to integer with a default value of 0. 32 | priority = (priority && Math.trunc(+priority)) || 0; 33 | 34 | if (priority < 0 || priority >= this._size) { 35 | priority = this._size - 1; // Put obj at the end of the line 36 | } 37 | 38 | this._slots[priority]!.push(obj); 39 | } 40 | 41 | dequeue(): ResourceRequest | null { 42 | // so priority equals 0 is the highest 43 | for (let i = 0, sl = this._slots.length; i < sl; i++) { 44 | if (this._slots[i]!.length > 0) { 45 | return this._slots[i]!.shift(); 46 | } 47 | } 48 | return null; 49 | } 50 | 51 | get head(): ResourceRequest | null { 52 | for (let i = 0, sl = this._slots.length; i < sl; i++) { 53 | if (this._slots[i]!.length > 0) { 54 | return this._slots[i]!.head; 55 | } 56 | } 57 | return null; 58 | } 59 | 60 | get tail(): ResourceRequest | null { 61 | for (let i = this._slots.length - 1; i >= 0; i--) { 62 | if (this._slots[i]!.length > 0) { 63 | return this._slots[i]!.tail; 64 | } 65 | } 66 | return null; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/pool-ts/Queue.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Deque } from './Deque.ts'; 4 | import { Node } from './DoublyLinkedList.ts'; 5 | import type { ResourceRequest } from './ResourceRequest.ts'; 6 | 7 | /** 8 | * Sort of a internal queue for holding the waiting 9 | * resource request for a given "priority". 10 | * Also handles managing timeouts rejections on items (is this the best place for this?) 11 | * This is the last point where we know which queue a resourceRequest is in 12 | */ 13 | export class Queue extends Deque> { 14 | /** 15 | * Adds the obj to the end of the list for this slot 16 | * we completely override the parent method because we need access to the 17 | * node for our rejection handler 18 | * @param {any} resourceRequest [description] 19 | */ 20 | override push(resourceRequest: ResourceRequest) { 21 | const node = new Node(resourceRequest); 22 | resourceRequest.promise.catch(this._createTimeoutRejectionHandler(node)); 23 | this._list.insertEnd(node); 24 | } 25 | 26 | _createTimeoutRejectionHandler(node: Node) { 27 | return (reason: any) => { 28 | if (reason.name === 'TimeoutError') { 29 | this._list.remove(node); 30 | } 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/pool-ts/ResourceLoan.ts: -------------------------------------------------------------------------------- 1 | import { Deferred } from './Deferred.ts'; 2 | import type { PooledResource } from './PooledResource.ts'; 3 | 4 | /** 5 | * Plan is to maybe add tracking via Error objects 6 | * and other fun stuff! 7 | */ 8 | export class ResourceLoan extends Deferred { 9 | private _creationTimestamp: number; 10 | pooledResource: PooledResource; 11 | 12 | /** 13 | * @param {any} pooledResource the PooledResource this loan belongs to 14 | * @param {PromiseConstructor} promiseConstructor promise implementation 15 | */ 16 | constructor(pooledResource: any, promiseConstructor: PromiseConstructor) { 17 | super(promiseConstructor); 18 | this._creationTimestamp = Date.now(); 19 | this.pooledResource = pooledResource; 20 | } 21 | 22 | override reject(): void { 23 | /** 24 | * Loans can only be resolved at the moment 25 | */ 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/pool-ts/ResourceRequest.ts: -------------------------------------------------------------------------------- 1 | import { Deferred } from './Deferred.ts'; 2 | import { TimeoutError } from './errors.ts'; 3 | 4 | function fbind(fn: () => void, ctx: any) { 5 | return function bound() { 6 | return fn.apply(ctx); 7 | }; 8 | } 9 | 10 | /** 11 | * Wraps a user's request for a resource 12 | * Basically a promise mashed in with a timeout 13 | * @private 14 | */ 15 | export class ResourceRequest extends Deferred { 16 | private _creationTimestamp: number; 17 | private _timeout: NodeJS.Timeout | null; 18 | 19 | /** 20 | * [constructor description] 21 | * @param {number} ttl timeout in milliseconds 22 | * @param {PromiseConstructor} promiseConstructor promise implementation 23 | */ 24 | constructor(ttl: number | undefined, promiseConstructor: PromiseConstructor) { 25 | super(promiseConstructor); 26 | this._creationTimestamp = Date.now(); 27 | this._timeout = null; 28 | 29 | if (ttl !== undefined) { 30 | this.setTimeout(ttl); 31 | } 32 | } 33 | 34 | setTimeout(delay: number): void { 35 | if (this._state !== ResourceRequest.PENDING) { 36 | return; 37 | } 38 | const ttl = Number.parseInt(delay.toString(), 10); 39 | 40 | if (Number.isNaN(ttl) || ttl <= 0) { 41 | throw new Error('delay must be a positive int'); 42 | } 43 | 44 | const age = Date.now() - this._creationTimestamp; 45 | 46 | if (this._timeout) { 47 | this.removeTimeout(); 48 | } 49 | 50 | this._timeout = setTimeout( 51 | fbind(this._fireTimeout, this), 52 | Math.max(ttl - age, 0), 53 | ); 54 | } 55 | 56 | removeTimeout(): void { 57 | if (this._timeout) { 58 | clearTimeout(this._timeout); 59 | } 60 | this._timeout = null; 61 | } 62 | 63 | private _fireTimeout(): void { 64 | this.reject(new TimeoutError('ResourceRequest timed out')); 65 | } 66 | 67 | override reject(reason?: any): void { 68 | this.removeTimeout(); 69 | super.reject(reason); 70 | } 71 | 72 | override resolve(value: T): void { 73 | this.removeTimeout(); 74 | super.resolve(value); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/pool-ts/errors.ts: -------------------------------------------------------------------------------- 1 | class ExtendableError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | this.name = this.constructor.name; 5 | this.message = message; 6 | 7 | if (typeof Error.captureStackTrace === 'function') { 8 | Error.captureStackTrace(this, this.constructor); 9 | } else { 10 | this.stack = new Error(message).stack; 11 | } 12 | } 13 | } 14 | 15 | class TimeoutError extends ExtendableError { 16 | constructor(message: string) { 17 | super(message); 18 | } 19 | } 20 | 21 | export { TimeoutError }; 22 | -------------------------------------------------------------------------------- /src/pool-ts/factoryValidator.ts: -------------------------------------------------------------------------------- 1 | interface Factory { 2 | create(): Promise; 3 | destroy(connection: T): Promise; 4 | validate?(connection: T): Promise; 5 | } 6 | 7 | export function validateFactory(factory: Factory): void { 8 | if (typeof factory.create !== 'function') { 9 | throw new TypeError('factory.create must be a function'); 10 | } 11 | 12 | if (typeof factory.destroy !== 'function') { 13 | throw new TypeError('factory.destroy must be a function'); 14 | } 15 | 16 | if (factory.validate !== undefined && typeof factory.validate !== 'function') { 17 | throw new TypeError('factory.validate must be a function'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pool-ts/types.ts: -------------------------------------------------------------------------------- 1 | import type { PooledResource } from './PooledResource.ts'; 2 | 3 | export interface IEvictorConfig { 4 | softIdleTimeoutMillis: number; 5 | idleTimeoutMillis: number; 6 | min: number; 7 | } 8 | 9 | export interface IEvictor { 10 | evict(config: IEvictorConfig, pooledResource: PooledResource, availableObjectsCount: number): boolean; 11 | } 12 | 13 | export interface Factory { 14 | create(): Promise; 15 | 16 | destroy(client: T): Promise; 17 | 18 | validate?(client: T): Promise; 19 | } 20 | 21 | export interface Options { 22 | max?: number; 23 | min?: number; 24 | maxWaitingClients?: number; 25 | testOnBorrow?: boolean; 26 | acquireTimeoutMillis?: number; 27 | destroyTimeoutMillis?: number; 28 | fifo?: boolean; 29 | priorityRange?: number; 30 | autostart?: boolean; 31 | evictionRunIntervalMillis?: number; 32 | numTestsPerEvictionRun?: number; 33 | softIdleTimeoutMillis?: number; 34 | idleTimeoutMillis?: number; 35 | } 36 | -------------------------------------------------------------------------------- /src/pool-ts/utils.ts: -------------------------------------------------------------------------------- 1 | function noop(): void {} 2 | 3 | /** 4 | * Reflects a promise but does not expose any 5 | * underlying value or rejection from that promise. 6 | * @param {Promise} promise - The promise to reflect. 7 | * @return {Promise} - A promise that resolves after the input promise settles without exposing its result. 8 | */ 9 | export function reflector(promise: Promise): Promise { 10 | return promise.then(noop, noop); 11 | } 12 | -------------------------------------------------------------------------------- /src/recycling-pool.ts: -------------------------------------------------------------------------------- 1 | import { DefaultEvictor } from './pool-ts/DefaultEvictor.ts'; 2 | import { Deque } from './pool-ts/Deque.ts'; 3 | import { Pool } from './pool-ts/Pool.ts'; 4 | import { PriorityQueue } from './pool-ts/PriorityQueue.ts'; 5 | import type { Factory, Options } from './pool-ts/types.ts'; 6 | 7 | export class RecyclingPool extends Pool { 8 | private recycleTimeout?: number; 9 | private recycleJitter?: number; 10 | 11 | constructor( 12 | factory: Factory, 13 | options: { 14 | recycleTimeout?: number; 15 | recycleJitter?: number; 16 | } & Options, 17 | ) { 18 | super(DefaultEvictor, Deque, PriorityQueue, factory, options); 19 | 20 | this.recycleTimeout = options.recycleTimeout ??= 900_000; // 15 min 21 | this.recycleJitter = options.recycleJitter ??= 60_000; // 1 min 22 | } 23 | 24 | override async release(resource: T) { 25 | const loan = this._resourceLoans.get(resource); 26 | const createdAt = loan === undefined ? 0 : loan.pooledResource.creationTime; 27 | 28 | // If the connection has been in use for longer than the recycleTimeoutMillis, then destroy it instead of releasing it back into the pool. 29 | // If that deletion brings the pool size below the min, the connection will be released (not destroyed) 30 | if ( 31 | new Date(createdAt + this.recycleTimeout! - (Math.random() * this.recycleJitter!)) 32 | <= new Date() && this._count - 1 >= this.min 33 | ) { 34 | return this.destroy(resource); 35 | } 36 | return super.release(resource); 37 | } 38 | } 39 | 40 | // Equivalent to createPool function from generic-pool 41 | export function createRecyclingPool( 42 | factory: Factory, 43 | options: { 44 | recycleTimeout?: number; 45 | recycleJitter?: number; 46 | } & Options, 47 | ) { 48 | return new RecyclingPool(factory, options); 49 | } 50 | -------------------------------------------------------------------------------- /src/sql-template-default.ts: -------------------------------------------------------------------------------- 1 | import duckdb from 'duckdb'; 2 | import type { RecyclingPool } from './recycling-pool.ts'; 3 | import type { SQLParamType } from './sql-template.ts'; 4 | import { SQLTemplate } from './sql-template.ts'; 5 | import { methodPromisify, stringifyArray } from './utils.ts'; 6 | 7 | const statementAllAsync = methodPromisify( 8 | duckdb.Statement.prototype.all, 9 | ); 10 | 11 | const prepareParams = (params: any[]) => { 12 | for (const [idx, param] of params.entries()) { 13 | if (typeof param === 'bigint') { 14 | // need to use toString because node duckdb driver can't handle node js BigInt type as parameter. 15 | params[idx] = `${param}`; 16 | continue; 17 | } 18 | 19 | if (typeof param === 'object' && !(param instanceof Date)) { 20 | if (Array.isArray(param)) { 21 | params[idx] = stringifyArray(param); 22 | continue; 23 | } 24 | 25 | params[idx] = JSON.stringify(param); 26 | continue; 27 | } 28 | } 29 | }; 30 | 31 | const transformResult = ( 32 | result: duckdb.TableData, 33 | columnInfo: duckdb.ColumnInfo[], 34 | ) => { 35 | const columnInfoObj: { [key: string]: duckdb.ColumnInfo } = {}; 36 | for (const colInfoI of columnInfo) { 37 | columnInfoObj[colInfoI.name] = colInfoI; 38 | } 39 | 40 | const data: { 41 | [columnName: string]: any; 42 | }[] = []; 43 | 44 | for (const row of result) { 45 | for (const colName of Object.keys(row)) { 46 | const columnType = columnInfoObj[colName]?.type; 47 | const value = row[colName]; 48 | 49 | const transformedValue = transformResultValue(value, columnType); 50 | row[colName] = transformedValue; 51 | } 52 | data.push(row); 53 | } 54 | 55 | return data; 56 | }; 57 | 58 | const transformResultValue = (value: any, columnType: duckdb.TypeInfo | undefined) => { 59 | if (value === null) return value; 60 | 61 | if (typeof value === 'string' && columnType?.alias === 'JSON') { 62 | return JSON.parse(value); 63 | } 64 | 65 | if (columnType?.id === 'ARRAY') { 66 | if ( 67 | columnType?.sql_type.includes('JSON') 68 | || columnType?.sql_type.includes('INTEGER') 69 | || columnType?.sql_type.includes('SMALLINT') 70 | || columnType?.sql_type.includes('TINYINT') 71 | || columnType?.sql_type.includes('DOUBLE') 72 | || columnType?.sql_type.includes('FLOAT') 73 | || columnType?.sql_type.includes('DECIMAL') 74 | || columnType?.sql_type.includes('BOOLEAN') 75 | ) { 76 | return JSON.parse(value); 77 | } 78 | 79 | return value; 80 | } 81 | 82 | if (columnType?.id === 'LIST') { 83 | return transformNDList(value, columnType); 84 | } 85 | 86 | return value; 87 | }; 88 | 89 | const transformNDList = (list: any[] | any, listType: duckdb.ListTypeInfo | duckdb.TypeInfo): any[] => { 90 | if (!Array.isArray(list)) { 91 | return transformResultValue(list, listType); 92 | } 93 | 94 | const nDList = []; 95 | for (const el of list) { 96 | nDList.push(transformNDList(el, (listType as duckdb.ListTypeInfo).child)); 97 | } 98 | 99 | return nDList; 100 | }; 101 | 102 | export class DefaultSQLTemplate extends SQLTemplate { 103 | constructor( 104 | protected strings: readonly string[], 105 | protected params: SQLParamType[], 106 | protected readonly pool: RecyclingPool, 107 | ) { 108 | super(); 109 | this.strings = strings; 110 | this.params = params; 111 | this.pool = pool; 112 | } 113 | 114 | protected async executeQuery() { 115 | // Implement your actual DB execution logic here 116 | // This could be a fetch or another async operation 117 | // gets connection from pool, runs query, release connection 118 | const { query, params } = this.toSQL(); 119 | let result; 120 | 121 | prepareParams(params); 122 | const db = await this.pool.acquire(); 123 | 124 | // wrapping duckdb driver error in new js error to add stack trace to it 125 | try { 126 | const statement = db.prepare(query); 127 | 128 | const duckdbResult = await statementAllAsync(statement, ...params); 129 | 130 | const columnInfo = statement.columns(); 131 | result = transformResult(duckdbResult, columnInfo) as T[]; 132 | } catch (error) { 133 | await this.pool.release(db); 134 | const newError = new Error((error as Error).message); 135 | throw newError; 136 | } 137 | 138 | await this.pool.release(db); 139 | 140 | return result; 141 | } 142 | 143 | async *stream() { 144 | let row: T; 145 | const { query, params } = this.toSQL(); 146 | 147 | prepareParams(params); 148 | 149 | const db = await this.pool.acquire(); 150 | 151 | // wrapping duckdb driver error in new js error to add stack trace to it 152 | try { 153 | const stream = db.stream(query, ...params); 154 | 155 | const asyncIterator = stream[Symbol.asyncIterator](); 156 | 157 | let iterResult = await asyncIterator.next(); 158 | while (!iterResult.done) { 159 | row = iterResult.value as T; 160 | yield row; 161 | 162 | iterResult = await asyncIterator.next(); 163 | } 164 | } catch (error) { 165 | await this.pool.release(db); 166 | const newError = new Error((error as Error).message); 167 | throw newError; 168 | } 169 | 170 | await this.pool.release(db); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/sql-template-neo.ts: -------------------------------------------------------------------------------- 1 | import type { DuckDBVector } from '@duckdb/node-api'; 2 | import { 3 | getColumnVectors, 4 | transformResultRowToObject, 5 | transformResultToObjects, 6 | } from './duckdb-neo/result-transformers.ts'; 7 | import { bindParams } from './duckdb-neo/utils.ts'; 8 | import type { RecyclingPool } from './recycling-pool.ts'; 9 | import type { SQLParamType } from './sql-template.ts'; 10 | import { SQLTemplate } from './sql-template.ts'; 11 | import type { DuckDBConnectionObj } from './types.ts'; 12 | 13 | export class NeoSQLTemplate extends SQLTemplate { 14 | constructor( 15 | protected strings: readonly string[], 16 | protected params: SQLParamType[], 17 | protected readonly pool: RecyclingPool, 18 | ) { 19 | super(); 20 | this.strings = strings; 21 | this.params = params; 22 | this.pool = pool; 23 | } 24 | 25 | protected async executeQuery() { 26 | // Implement your actual DB execution logic here 27 | // This could be a fetch or another async operation 28 | // gets connection from pool, runs query, release connection 29 | const { query, params } = this.toSQL(); 30 | let result; 31 | 32 | const connObj = await this.pool.acquire(); 33 | // console.log('connObj:', connObj); 34 | 35 | // wrapping duckdb driver error in new js error to add stack trace to it 36 | try { 37 | const prepared = await connObj.connection.prepare(query); 38 | bindParams(prepared, params); 39 | 40 | const duckDbResult = await prepared.run().finally(); 41 | result = await transformResultToObjects(duckDbResult) as T[]; 42 | } catch (error) { 43 | await this.pool.release(connObj); 44 | const newError = new Error((error as Error).message); 45 | throw newError; 46 | } 47 | 48 | await this.pool.release(connObj); 49 | 50 | return result; 51 | } 52 | 53 | async *stream() { 54 | const { query, params } = this.toSQL(); 55 | 56 | const connObj = await this.pool.acquire(); 57 | 58 | // wrapping duckdb driver error in new js error to add stack trace to it 59 | try { 60 | const prepared = await connObj.connection.prepare(query); 61 | bindParams(prepared, params); 62 | 63 | const duckDbResult = await prepared.run(); 64 | 65 | for (;;) { 66 | const chunk = await duckDbResult.fetchChunk(); 67 | if (chunk.rowCount === 0) { 68 | break; 69 | } 70 | 71 | const columnVectors: DuckDBVector[] = getColumnVectors(chunk); 72 | 73 | for (let rowIndex = 0; rowIndex < chunk.rowCount; rowIndex++) { 74 | const row = transformResultRowToObject(duckDbResult, columnVectors, rowIndex) as T; 75 | yield row; 76 | } 77 | } 78 | } catch (error) { 79 | await this.pool.release(connObj); 80 | const newError = new Error((error as Error).message); 81 | throw newError; 82 | } 83 | 84 | await this.pool.release(connObj); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/sql-template.ts: -------------------------------------------------------------------------------- 1 | import type { RecyclingPool } from './recycling-pool.ts'; 2 | import type { JSONArray, JSONObject, ParamType } from './types.ts'; 3 | 4 | export type SQLParamType = 5 | | string 6 | | number 7 | | bigint 8 | | Date 9 | | boolean 10 | | null 11 | | JSONArray 12 | | JSONObject 13 | | SQLIdentifier 14 | | SQLValues 15 | | SQLRaw 16 | | SQLDefault; 17 | 18 | export abstract class SQLTemplate { 19 | protected abstract strings: readonly string[]; 20 | protected abstract params: SQLParamType[]; 21 | protected abstract readonly pool: RecyclingPool; 22 | 23 | append(value: SQLTemplate) { 24 | this.strings = [ 25 | ...this.strings.slice(0, -1), 26 | `${this.strings.at(-1)}${value.strings.at(0)}`, 27 | ...value.strings.slice(1), 28 | ]; 29 | this.params = [...this.params, ...value.params]; 30 | } 31 | 32 | // Method to extract raw SQL 33 | toSQL() { 34 | if (this.params.length === 0) { 35 | return { query: this.strings[0] ?? '', params: [] }; 36 | } 37 | 38 | const filteredParams: ParamType[] = []; 39 | let query = '', 40 | idxShift = 0, 41 | param: any; 42 | for (const [idx, stringI] of this.strings.entries()) { 43 | if (idx === this.strings.length - 1) { 44 | query += stringI; 45 | continue; 46 | } 47 | 48 | param = this.params[idx]; 49 | let typedPlaceholder: string; 50 | if ( 51 | param instanceof SQLIdentifier 52 | || param instanceof SQLValues 53 | || param instanceof SQLRaw 54 | || param instanceof SQLDefault 55 | ) { 56 | typedPlaceholder = param.generateSQL(); 57 | idxShift += 1; 58 | query += stringI + typedPlaceholder; 59 | continue; 60 | } 61 | 62 | if (param === undefined) { 63 | throw new Error("you can't specify undefined as parameter"); 64 | } 65 | 66 | if (typeof param === 'symbol') { 67 | throw new Error("you can't specify symbol as parameter"); 68 | } 69 | 70 | if (typeof param === 'function') { 71 | throw new Error("you can't specify function as parameter"); 72 | } 73 | 74 | typedPlaceholder = `$${idx + 1 - idxShift}`; 75 | query += stringI + typedPlaceholder; 76 | filteredParams.push(param); 77 | } 78 | 79 | return { 80 | query, 81 | params: filteredParams, 82 | }; 83 | } 84 | 85 | // Allow it to be awaited (like a Promise) 86 | then( 87 | onfulfilled?: 88 | | ((value: T[]) => TResult1 | PromiseLike) 89 | | null 90 | | undefined, 91 | onrejected?: 92 | | ((reason: any) => TResult2 | PromiseLike) 93 | | null 94 | | undefined, 95 | ): Promise { 96 | // Here you could handle the query execution logic (replace with your own) 97 | const result = this.executeQuery(); 98 | return Promise.resolve(result).then(onfulfilled, onrejected); 99 | } 100 | 101 | protected abstract executeQuery(): Promise; 102 | 103 | abstract stream(): AsyncGenerator, void, unknown>; 104 | 105 | async *chunked(chunkSize: number = 1) { 106 | let rows: T[] = []; 107 | let row: T; 108 | const asyncIterator = this.stream(); 109 | let iterResult = await asyncIterator.next(); 110 | 111 | while (!iterResult.done) { 112 | row = iterResult.value as T; 113 | rows.push(row); 114 | 115 | if (rows.length % chunkSize === 0) { 116 | yield rows; 117 | rows = []; 118 | } 119 | 120 | iterResult = await asyncIterator.next(); 121 | } 122 | 123 | if (rows.length !== 0) { 124 | yield rows; 125 | } 126 | } 127 | } 128 | 129 | export type Identifier = 130 | | string 131 | | string[] 132 | | { schema?: string; table?: string; column?: string; as?: string } 133 | | { 134 | schema?: string; 135 | table?: string; 136 | column?: string; 137 | as?: string; 138 | }[]; 139 | 140 | export class SQLIdentifier { 141 | constructor(private readonly value: Identifier) {} 142 | 143 | // TODO: @AlexBlokh do error's text 144 | static checkObject(object: { 145 | schema?: string; 146 | table?: string; 147 | column?: string; 148 | as?: string; 149 | }) { 150 | if (Object.values(object).includes(undefined!)) { 151 | throw new Error( 152 | `you can't specify undefined parameters. maybe you want to omit it?`, 153 | ); 154 | } 155 | 156 | if (Object.keys(object).length === 0) { 157 | throw new Error(`you need to specify at least one parameter.`); 158 | } 159 | 160 | if ( 161 | object.schema !== undefined 162 | && object.table === undefined 163 | && object.column !== undefined 164 | ) { 165 | throw new Error( 166 | `you can't specify only "schema" and "column" properties, you need also specify "table".`, 167 | ); 168 | } 169 | 170 | if (Object.keys(object).length === 1 && object.as !== undefined) { 171 | throw new Error(`you can't specify only "as" property.`); 172 | } 173 | 174 | if ( 175 | object.as !== undefined 176 | && object.column === undefined 177 | && object.table === undefined 178 | ) { 179 | throw new Error( 180 | `you have to specify "column" or "table" property along with "as".`, 181 | ); 182 | } 183 | 184 | if ( 185 | !['string', 'undefined'].includes(typeof object.schema) 186 | || !['string', 'undefined'].includes(typeof object.table) 187 | || !['string', 'undefined'].includes(typeof object.column) 188 | || !['string', 'undefined'].includes(typeof object.as) 189 | ) { 190 | throw new Error( 191 | "object properties 'schema', 'table', 'column', 'as' should be of string type or omitted.", 192 | ); 193 | } 194 | } 195 | 196 | static objectToSQL(object: { 197 | schema?: string; 198 | table?: string; 199 | column?: string; 200 | as?: string; 201 | }) { 202 | SQLIdentifier.checkObject(object); 203 | 204 | const schema = object.schema === undefined ? '' : `"${object.schema}".`; 205 | const table = object.table === undefined ? '' : `"${object.table}"`; 206 | const column = object.column === undefined ? '' : `."${object.column}"`; 207 | const as = object.as === undefined ? '' : ` as "${object.as}"`; 208 | 209 | return `${schema}${table}${column}${as}`.replace(/^\.|\.$/g, ''); 210 | } 211 | 212 | generateSQL() { 213 | if (typeof this.value === 'string') { 214 | return `"${this.value}"`; 215 | } 216 | 217 | if (Array.isArray(this.value)) { 218 | if (this.value.length === 0) { 219 | throw new Error( 220 | `you can't specify empty array as parameter for sql.identifier.`, 221 | ); 222 | } 223 | 224 | if (this.value.every((val) => typeof val === 'string')) { 225 | return `"${this.value.join('", "')}"`; 226 | } 227 | 228 | if ( 229 | this.value.every( 230 | (val) => typeof val === 'object' && !Array.isArray(val) && val !== null, 231 | ) 232 | ) { 233 | return `${ 234 | this.value 235 | .map((element) => SQLIdentifier.objectToSQL(element)) 236 | .join(', ') 237 | }`; 238 | } 239 | 240 | let areThereAnyArrays = false; 241 | for (const val of this.value) { 242 | if (Array.isArray(val)) { 243 | areThereAnyArrays = true; 244 | break; 245 | } 246 | } 247 | if (areThereAnyArrays) { 248 | throw new Error( 249 | `you can't specify array of arrays as parameter for sql.identifier.`, 250 | ); 251 | } 252 | 253 | throw new Error( 254 | `you can't specify array of (null or undefined or number or bigint or boolean or symbol or function) as parameter for sql.identifier.`, 255 | ); 256 | } 257 | 258 | if (typeof this.value === 'object' && this.value !== null) { 259 | // typeof this.value === "object" 260 | return SQLIdentifier.objectToSQL(this.value); 261 | } 262 | 263 | if (this.value === null) { 264 | throw new Error( 265 | `you can't specify null as parameter for sql.identifier.`, 266 | ); 267 | } 268 | 269 | throw new Error( 270 | `you can't specify ${typeof this.value} as parameter for sql.identifier.`, 271 | ); 272 | } 273 | } 274 | 275 | export type Value = string | number | bigint | boolean | Date | SQLDefault | null | Value[]; 276 | export type Values = Value[][]; 277 | export class SQLValues { 278 | constructor(private readonly value: Values) {} 279 | 280 | generateSQL() { 281 | if (!Array.isArray(this.value)) { 282 | if (this.value === null) throw new Error(`you can't specify null as parameter for sql.values.`); 283 | throw new Error(`you can't specify ${typeof this.value} as parameter for sql.values.`); 284 | } 285 | 286 | if (this.value.length === 0) { 287 | throw new Error(`you can't specify empty array as parameter for sql.values.`); 288 | } 289 | 290 | return this.value 291 | .map((val) => SQLValues.arrayToSQL(val)) 292 | .join(', '); 293 | } 294 | 295 | private static arrayToSQL(array: Value[]) { 296 | if (Array.isArray(array)) { 297 | if (array.length === 0) { 298 | throw new Error(`array of values can't be empty.`); 299 | } 300 | 301 | return `(${array.map((val) => SQLValues.valueToSQL(val)).join(', ')})`; 302 | } 303 | 304 | if (array === null) throw new Error(`you can't specify array of null as parameter for sql.values.`); 305 | throw new Error(`you can't specify array of ${typeof array} as parameter for sql.values.`); 306 | } 307 | 308 | private static valueToSQL(value: Value): string { 309 | if ( 310 | typeof value === 'number' 311 | || typeof value === 'bigint' 312 | || typeof value === 'boolean' 313 | || value === null 314 | ) { 315 | return `${value}`; 316 | } 317 | 318 | if (value instanceof SQLDefault) { 319 | return value.generateSQL(); 320 | } 321 | 322 | if (value instanceof Date) { 323 | return `'${value.toISOString()}'`; 324 | } 325 | 326 | if (typeof value === 'string') { 327 | return `'${value}'`; 328 | } 329 | 330 | if (Array.isArray(value)) { 331 | return `[${value.map((arrayValue) => SQLValues.valueToSQL(arrayValue))}]`; 332 | } 333 | 334 | if (typeof value === 'object') { 335 | // object case 336 | throw new Error( 337 | "value can't be object. you can't specify [ [ {...}, ...], ...] as parameter for sql.values.", 338 | ); 339 | } 340 | 341 | if (value === undefined) { 342 | throw new Error("value can't be undefined, maybe you mean sql.default?"); 343 | } 344 | 345 | throw new Error(`you can't specify ${typeof value} as value.`); 346 | } 347 | } 348 | 349 | export type Raw = string | number | boolean | bigint; 350 | export class SQLRaw { 351 | constructor(private readonly value: Raw) {} 352 | 353 | generateSQL() { 354 | if ( 355 | typeof this.value === 'number' 356 | || typeof this.value === 'bigint' 357 | || typeof this.value === 'string' 358 | || typeof this.value === 'boolean' 359 | ) { 360 | return `${this.value}`; 361 | } 362 | 363 | if (typeof this.value === 'object') { 364 | throw new Error( 365 | "you can't specify array, object or null as parameter for sql.raw.", 366 | ); 367 | } 368 | 369 | if (this.value === undefined) { 370 | throw new Error( 371 | "you can't specify undefined as parameter for sql.raw, maybe you mean using sql.default?", 372 | ); 373 | } 374 | 375 | if (typeof this.value === 'symbol') { 376 | throw new Error("you can't specify symbol as parameter for sql.raw."); 377 | } 378 | 379 | throw new Error("you can't specify function as parameter for sql.raw."); 380 | } 381 | } 382 | 383 | export class SQLDefault { 384 | generateSQL() { 385 | return 'default'; 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { DuckDBConnection, DuckDBInstance } from "@duckdb/node-api"; 2 | 3 | export type ParamType = 4 | | string 5 | | number 6 | | bigint 7 | | Date 8 | | boolean 9 | | null; 10 | 11 | export type UnsafeParamType = 12 | | string 13 | | number 14 | | bigint 15 | | Date 16 | | boolean 17 | | null 18 | | JSONObject 19 | | JSONArray; 20 | 21 | type ValueForArray = number | bigint | boolean | null | Date | JSONObject | JSONArray; 22 | type ValueForObject = string | number | bigint | boolean | null | Date | JSONObject | Array; 23 | 24 | export type JSONArray = Array; 25 | 26 | export type JSONObject = { [key: string]: ValueForObject }; 27 | 28 | export interface DuckDBConnectionObj { 29 | instance: DuckDBInstance; 30 | connection: DuckDBConnection; 31 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { promisify } from 'util'; 2 | 3 | export function methodPromisify( 4 | methodFn: (...args: any[]) => any, 5 | ): (target: T, ...args: any[]) => Promise { 6 | return promisify((target: T, ...args: any[]): any => methodFn.bind(target)(...args)) as any; 7 | } 8 | 9 | export const stringifyArray = (array: any[] | any): string => { 10 | if (!Array.isArray(array)) { 11 | return transformValueForArray(array); 12 | } 13 | 14 | let returnStr = '['; 15 | for (const [idx, el] of array.entries()) { 16 | returnStr += `${stringifyArray(el)}`; 17 | 18 | if (idx === array.length - 1) continue; 19 | returnStr += ','; 20 | } 21 | 22 | returnStr += ']'; 23 | 24 | return returnStr; 25 | }; 26 | 27 | export const transformValueForArray = (value: any) => { 28 | if ( 29 | value === null 30 | || typeof value === 'number' 31 | || typeof value === 'boolean' 32 | || typeof value === 'bigint' 33 | ) return value; 34 | 35 | if (value instanceof Date) { 36 | return value.toISOString(); 37 | } 38 | 39 | if (typeof value === 'object') { 40 | return JSON.stringify(value); 41 | } 42 | 43 | if (value === undefined) { 44 | throw new Error("you can't specify undefined as array value."); 45 | } 46 | 47 | if (typeof value === 'string') { 48 | throw new Error("you can't specify string as array value."); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /tests/neo-waddler-unit.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, expect, test } from 'vitest'; 2 | import { waddler } from '../src/neo.ts'; 3 | 4 | 5 | let sql: ReturnType; 6 | beforeAll(async () => { 7 | sql = waddler({ url: ':memory:', max: 10, accessMode: 'read_write' }); 8 | }); 9 | 10 | // UNSAFE------------------------------------------------------------------- 11 | test('all types test', async () => { 12 | await sql.unsafe(`create table all_types ( 13 | smallint_ smallint, 14 | integer_ integer, 15 | bigint_ bigint, 16 | double_ double, 17 | varchar_ varchar, 18 | boolean_ boolean, 19 | time_ time, 20 | date_ date, 21 | timestamp_ timestamp, 22 | json_ json, 23 | arrayInt integer[3], 24 | listInt integer[] 25 | );`); 26 | 27 | const date = new Date('2024-10-31T14:25:29.425Z'); 28 | await sql.unsafe( 29 | `insert into all_types values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12);`, 30 | [ 31 | 1, 32 | 10, 33 | BigInt('9007199254740992') + BigInt(1), 34 | 20.4, 35 | 'qwerty', 36 | true, 37 | date, 38 | date, 39 | date, 40 | { name: 'alex', age: 26, bookIds: [1, 2, 3], vacationRate: 2.5, aliases: ['sasha', 'sanya'], isMarried: true }, 41 | [1, 2, 3], 42 | [1, 2, 3, 4, 5], 43 | ], 44 | { rowMode: 'default' }, 45 | ); 46 | 47 | let res = await sql.unsafe(`select * from all_types;`); 48 | 49 | const dateWithoutTime = new Date(date); 50 | dateWithoutTime.setUTCHours(0, 0, 0, 0); 51 | const expectedRes = { 52 | smallint_: 1, 53 | integer_: 10, 54 | bigint_: BigInt('9007199254740993'), 55 | double_: 20.4, 56 | varchar_: 'qwerty', 57 | boolean_: true, 58 | time_: '14:25:29.425', 59 | date_: dateWithoutTime, 60 | timestamp_: date, 61 | json_: { 62 | name: 'alex', 63 | age: 26, 64 | bookIds: [1, 2, 3], 65 | vacationRate: 2.5, 66 | aliases: ['sasha', 'sanya'], 67 | isMarried: true, 68 | }, 69 | arrayInt: [1, 2, 3], 70 | listInt: [1, 2, 3, 4, 5], 71 | }; 72 | expect(res[0]).toStrictEqual(expectedRes); 73 | 74 | // same as select query as above but with rowMode: "array" 75 | res = await sql.unsafe(`select * from all_types;`, [], { rowMode: 'array' }); 76 | expect(res[0]).toStrictEqual(Object.values(expectedRes)); 77 | 78 | // float 79 | await sql.unsafe(`create table float_table ( 80 | float_ float 81 | );`); 82 | 83 | await sql.unsafe( 84 | `insert into float_table values ($1)`, 85 | [20.3], 86 | { rowMode: 'default' }, 87 | ); 88 | 89 | res = await sql.unsafe(`select * from float_table;`); 90 | 91 | expect((res[0] as { float_: number })['float_'].toFixed(1)).toEqual('20.3'); 92 | 93 | // map 94 | await sql.unsafe(`create table map_table ( 95 | map_ map(map(varchar, integer), double) 96 | );`); 97 | 98 | await sql.unsafe( 99 | `insert into map_table values ( 100 | MAP {MAP {'a': 42.001, 'b': -32.1}: 42.001, MAP {'a1': 42.001, 'b1': -32.1}: -32.1} 101 | )`, 102 | [], 103 | { rowMode: 'default' }, 104 | ); 105 | 106 | const expectedMap = new Map([ 107 | [new Map([['a', 42], ['b', -32]]), 42.001], 108 | [new Map([['a1', 42], ['b1', -32]]), -32.1], 109 | ]); 110 | res = await sql.unsafe(`select * from map_table;`); 111 | 112 | expect(res[0]).toStrictEqual({ map_: expectedMap }); 113 | 114 | // TODO: add tests for select when the columns are of type: map, struct, union, ... 115 | }); 116 | 117 | test('array type test', async () => { 118 | await sql.unsafe(`create table array_table ( 119 | arrayInt integer[3], 120 | arrayDouble double[3], 121 | arrayBoolean boolean[3], 122 | arrayBigint bigint[3], 123 | arrayDate date[3], 124 | arrayTime time[3], 125 | arrayTimestamp timestamp[3], 126 | arrayJson json[2] 127 | );`); 128 | 129 | const dates = [ 130 | new Date('2024-10-31T14:25:29.425Z'), 131 | new Date('2024-10-30T14:25:29.425Z'), 132 | new Date('2024-10-29T14:25:29.425Z'), 133 | ]; 134 | await sql.unsafe(`insert into array_table values ($1, $2, $3, $4, $5, $6, $7, $8);`, [ 135 | [1, 2, 3], 136 | [1.5, 2.6, 3.9], 137 | [true, false, true], 138 | [ 139 | BigInt('9007199254740992') + BigInt(1), 140 | BigInt('9007199254740992') + BigInt(3), 141 | BigInt('9007199254740992') + BigInt(5), 142 | ], 143 | dates, 144 | dates, 145 | dates, 146 | [ 147 | { name: 'alex', age: 26, bookIds: [1, 2, 3], aliases: ['sasha', 'sanya'] }, 148 | { name: 'oleksii', age: 21, bookIds: [1, 2, 4], aliases: ['leha'] }, 149 | ], 150 | ]); 151 | 152 | const res = await sql.unsafe('select * from array_table;'); 153 | 154 | const datesWithoutTime = [...dates]; 155 | for (const date of datesWithoutTime) date.setUTCHours(0, 0, 0, 0); 156 | 157 | const expectedRes = { 158 | arrayInt: [1, 2, 3], 159 | arrayDouble: [1.5, 2.6, 3.9], 160 | arrayBoolean: [true, false, true], 161 | arrayBigint: [BigInt('9007199254740993'), BigInt('9007199254740995'), BigInt('9007199254740997')], 162 | arrayDate: datesWithoutTime, 163 | arrayTime: ['14:25:29.425', '14:25:29.425', '14:25:29.425'], 164 | arrayTimestamp: [ 165 | new Date('2024-10-31T14:25:29.425Z'), 166 | new Date('2024-10-30T14:25:29.425Z'), 167 | new Date('2024-10-29T14:25:29.425Z'), 168 | ], 169 | arrayJson: [ 170 | { name: 'alex', age: 26, bookIds: [1, 2, 3], aliases: ['sasha', 'sanya'] }, 171 | { name: 'oleksii', age: 21, bookIds: [1, 2, 4], aliases: ['leha'] }, 172 | ], 173 | }; 174 | 175 | expect(res[0]).toStrictEqual(expectedRes); 176 | }); 177 | 178 | test('nested 2d array type test', async () => { 179 | await sql.unsafe(`create table nested_array_table ( 180 | arrayInt integer[3][2], 181 | arrayDouble double[3][2], 182 | arrayBoolean boolean[3][2], 183 | arrayBigint bigint[3][2], 184 | arrayDate date[3][2], 185 | arrayTime time[3][2], 186 | arrayTimestamp timestamp[3][2] 187 | );`); 188 | 189 | const dates = [ 190 | new Date('2024-10-31T14:25:29.425Z'), 191 | new Date('2024-10-30T14:25:29.425Z'), 192 | new Date('2024-10-29T14:25:29.425Z'), 193 | ]; 194 | await sql.unsafe(`insert into nested_array_table values ($1, $2, $3, $4, $5, $6, $7);`, [ 195 | [[1, 2, 3], [1, 2, 3]], 196 | [[1.5, 2.6, 3.9], [1.5, 2.6, 3.9]], 197 | [[true, false, true], [true, false, true]], 198 | [ 199 | [ 200 | BigInt('9007199254740992') + BigInt(1), 201 | BigInt('9007199254740992') + BigInt(3), 202 | BigInt('9007199254740992') + BigInt(5), 203 | ], 204 | [ 205 | BigInt('9007199254740992') + BigInt(1), 206 | BigInt('9007199254740992') + BigInt(3), 207 | BigInt('9007199254740992') + BigInt(5), 208 | ], 209 | ], 210 | [dates, dates], 211 | [dates, dates], 212 | [dates, dates], 213 | ]); 214 | 215 | const res = await sql.unsafe('select * from nested_array_table;'); 216 | 217 | const datesWithoutTime = [...dates]; 218 | for (const date of datesWithoutTime) date.setUTCHours(0, 0, 0, 0); 219 | 220 | const expectedRes = { 221 | arrayInt: [[1, 2, 3], [1, 2, 3]], 222 | arrayDouble: [[1.5, 2.6, 3.9], [1.5, 2.6, 3.9]], 223 | arrayBoolean: [[true, false, true], [true, false, true]], 224 | arrayBigint: [ 225 | [ 226 | BigInt('9007199254740992') + BigInt(1), 227 | BigInt('9007199254740992') + BigInt(3), 228 | BigInt('9007199254740992') + BigInt(5), 229 | ], 230 | [ 231 | BigInt('9007199254740992') + BigInt(1), 232 | BigInt('9007199254740992') + BigInt(3), 233 | BigInt('9007199254740992') + BigInt(5), 234 | ], 235 | ], 236 | arrayDate: [datesWithoutTime, datesWithoutTime], 237 | arrayTime: [['14:25:29.425', '14:25:29.425', '14:25:29.425'], ['14:25:29.425', '14:25:29.425', '14:25:29.425']], 238 | arrayTimestamp: [ 239 | [ 240 | new Date('2024-10-31T14:25:29.425Z'), 241 | new Date('2024-10-30T14:25:29.425Z'), 242 | new Date('2024-10-29T14:25:29.425Z'), 243 | ], 244 | [ 245 | new Date('2024-10-31T14:25:29.425Z'), 246 | new Date('2024-10-30T14:25:29.425Z'), 247 | new Date('2024-10-29T14:25:29.425Z'), 248 | ], 249 | ], 250 | }; 251 | 252 | expect(res[0]).toStrictEqual(expectedRes); 253 | }); 254 | 255 | test('nested 3d array type test', async () => { 256 | await sql.unsafe(`create table nested_3d_array_table ( 257 | arrayInt integer[3][2][2], 258 | arrayDouble double[3][2][2], 259 | arrayBoolean boolean[3][2][2], 260 | arrayBigint bigint[3][2][2], 261 | arrayDate date[3][2][2], 262 | arrayTime time[3][2][2], 263 | arrayTimestamp timestamp[3][2][2] 264 | );`); 265 | 266 | const dates = [ 267 | new Date('2024-10-31T14:25:29.425Z'), 268 | new Date('2024-10-30T14:25:29.425Z'), 269 | new Date('2024-10-29T14:25:29.425Z'), 270 | ]; 271 | await sql.unsafe(`insert into nested_3d_array_table values ($1, $2, $3, $4, $5, $6, $7);`, [ 272 | [[[1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3]]], 273 | [[[1.5, 2.6, 3.9], [1.5, 2.6, 3.9]], [[1.5, 2.6, 3.9], [1.5, 2.6, 3.9]]], 274 | [[[true, false, true], [true, false, true]], [[true, false, true], [true, false, true]]], 275 | [ 276 | [ 277 | [ 278 | BigInt('9007199254740992') + BigInt(1), 279 | BigInt('9007199254740992') + BigInt(3), 280 | BigInt('9007199254740992') + BigInt(5), 281 | ], 282 | [ 283 | BigInt('9007199254740992') + BigInt(1), 284 | BigInt('9007199254740992') + BigInt(3), 285 | BigInt('9007199254740992') + BigInt(5), 286 | ], 287 | ], 288 | [ 289 | [ 290 | BigInt('9007199254740992') + BigInt(1), 291 | BigInt('9007199254740992') + BigInt(3), 292 | BigInt('9007199254740992') + BigInt(5), 293 | ], 294 | [ 295 | BigInt('9007199254740992') + BigInt(1), 296 | BigInt('9007199254740992') + BigInt(3), 297 | BigInt('9007199254740992') + BigInt(5), 298 | ], 299 | ], 300 | ], 301 | [[dates, dates], [dates, dates]], 302 | [[dates, dates], [dates, dates]], 303 | [[dates, dates], [dates, dates]], 304 | ]); 305 | 306 | const res = await sql.unsafe('select * from nested_3d_array_table;'); 307 | 308 | const datesWithoutTime = [...dates]; 309 | for (const date of datesWithoutTime) date.setUTCHours(0, 0, 0, 0); 310 | 311 | const expectedRes = { 312 | arrayInt: [[[1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3]]], 313 | arrayDouble: [[[1.5, 2.6, 3.9], [1.5, 2.6, 3.9]], [[1.5, 2.6, 3.9], [1.5, 2.6, 3.9]]], 314 | arrayBoolean: [[[true, false, true], [true, false, true]], [[true, false, true], [true, false, true]]], 315 | arrayBigint: [ 316 | [ 317 | [BigInt('9007199254740993'), BigInt('9007199254740995'), BigInt('9007199254740997')], 318 | [BigInt('9007199254740993'), BigInt('9007199254740995'), BigInt('9007199254740997')], 319 | ], 320 | [ 321 | [BigInt('9007199254740993'), BigInt('9007199254740995'), BigInt('9007199254740997')], 322 | [BigInt('9007199254740993'), BigInt('9007199254740995'), BigInt('9007199254740997')], 323 | ], 324 | ], 325 | arrayDate: [[datesWithoutTime, datesWithoutTime], [datesWithoutTime, datesWithoutTime]], 326 | arrayTime: [ 327 | [ 328 | ['14:25:29.425', '14:25:29.425', '14:25:29.425'], 329 | ['14:25:29.425', '14:25:29.425', '14:25:29.425'], 330 | ], 331 | [ 332 | ['14:25:29.425', '14:25:29.425', '14:25:29.425'], 333 | ['14:25:29.425', '14:25:29.425', '14:25:29.425'], 334 | ], 335 | ], 336 | arrayTimestamp: [ 337 | [ 338 | [ 339 | new Date('2024-10-31T14:25:29.425Z'), 340 | new Date('2024-10-30T14:25:29.425Z'), 341 | new Date('2024-10-29T14:25:29.425Z'), 342 | ], 343 | [ 344 | new Date('2024-10-31T14:25:29.425Z'), 345 | new Date('2024-10-30T14:25:29.425Z'), 346 | new Date('2024-10-29T14:25:29.425Z'), 347 | ], 348 | ], 349 | [ 350 | [ 351 | new Date('2024-10-31T14:25:29.425Z'), 352 | new Date('2024-10-30T14:25:29.425Z'), 353 | new Date('2024-10-29T14:25:29.425Z'), 354 | ], 355 | [ 356 | new Date('2024-10-31T14:25:29.425Z'), 357 | new Date('2024-10-30T14:25:29.425Z'), 358 | new Date('2024-10-29T14:25:29.425Z'), 359 | ], 360 | ], 361 | ], 362 | }; 363 | 364 | expect(res[0]).toStrictEqual(expectedRes); 365 | }); 366 | 367 | test('list type test', async () => { 368 | await sql.unsafe(`create table list_table ( 369 | listInt integer[], 370 | listDouble double[], 371 | listBoolean boolean[], 372 | listBigint bigint[], 373 | listDate date[], 374 | listTime time[], 375 | listTimestamp timestamp[], 376 | listJson json[] 377 | );`); 378 | 379 | const dates = [ 380 | new Date('2024-10-31T14:25:29.425Z'), 381 | new Date('2024-10-30T14:25:29.425Z'), 382 | new Date('2024-10-29T14:25:29.425Z'), 383 | ]; 384 | await sql.unsafe(`insert into list_table values ($1, $2, $3, $4, $5, $6, $7, $8);`, [ 385 | [1, 2, 3, 1234, 34], 386 | [1.5, 2.6, 3.9, 100.345], 387 | [true, false], 388 | [ 389 | BigInt('9007199254740992') + BigInt(1), 390 | BigInt('9007199254740992') + BigInt(3), 391 | BigInt('9007199254740992') + BigInt(5), 392 | ], 393 | dates, 394 | dates, 395 | dates, 396 | [ 397 | { name: 'alex', age: 26, bookIds: [1, 2, 3], aliases: ['sasha', 'sanya'] }, 398 | { name: 'oleksii', age: 21, bookIds: [1, 2, 4], aliases: ['leha'] }, 399 | { name: 'oleksii', age: 24 }, 400 | ], 401 | ]); 402 | 403 | const res = await sql.unsafe('select * from list_table;'); 404 | 405 | const datesWithoutTime = [...dates]; 406 | for (const date of datesWithoutTime) date.setUTCHours(0, 0, 0, 0); 407 | 408 | const expectedRes = { 409 | listInt: [1, 2, 3, 1234, 34], 410 | listDouble: [1.5, 2.6, 3.9, 100.345], 411 | listBoolean: [true, false], 412 | listBigint: [BigInt('9007199254740993'), BigInt('9007199254740995'), BigInt('9007199254740997')], 413 | listDate: datesWithoutTime, 414 | listTime: ['14:25:29.425', '14:25:29.425', '14:25:29.425'], 415 | listTimestamp: [ 416 | new Date('2024-10-31T14:25:29.425Z'), 417 | new Date('2024-10-30T14:25:29.425Z'), 418 | new Date('2024-10-29T14:25:29.425Z'), 419 | ], 420 | listJson: [ 421 | { name: 'alex', age: 26, bookIds: [1, 2, 3], aliases: ['sasha', 'sanya'] }, 422 | { name: 'oleksii', age: 21, bookIds: [1, 2, 4], aliases: ['leha'] }, 423 | { name: 'oleksii', age: 24 }, 424 | ], 425 | }; 426 | 427 | expect(res[0]).toStrictEqual(expectedRes); 428 | }); 429 | 430 | test('nested 2d list type test', async () => { 431 | await sql.unsafe(`create table nested_list_table ( 432 | listInt integer[][], 433 | listDouble double[][], 434 | listBoolean boolean[][], 435 | listBigint bigint[][], 436 | listDate date[][], 437 | listTime time[][], 438 | listTimestamp timestamp[][] 439 | );`); 440 | 441 | const dates = [ 442 | new Date('2024-10-31T14:25:29.425Z'), 443 | new Date('2024-10-30T14:25:29.425Z'), 444 | new Date('2024-10-29T14:25:29.425Z'), 445 | ]; 446 | await sql.unsafe(`insert into nested_list_table values ($1, $2, $3, $4, $5, $6, $7);`, [ 447 | [[1, 2, 3], [1, 2, 3]], 448 | [[1.5, 2.6, 3.9], [1.5, 2.6, 3.9]], 449 | [[true, false, true], [true, false, true]], 450 | [ 451 | [ 452 | BigInt('9007199254740992') + BigInt(1), 453 | BigInt('9007199254740992') + BigInt(3), 454 | BigInt('9007199254740992') + BigInt(5), 455 | ], 456 | [ 457 | BigInt('9007199254740992') + BigInt(1), 458 | BigInt('9007199254740992') + BigInt(3), 459 | BigInt('9007199254740992') + BigInt(5), 460 | ], 461 | ], 462 | [dates, dates], 463 | [dates, dates], 464 | [dates, dates], 465 | ]); 466 | 467 | const res = await sql.unsafe('select * from nested_list_table;'); 468 | 469 | const datesWithoutTime = [...dates]; 470 | for (const date of datesWithoutTime) date.setUTCHours(0, 0, 0, 0); 471 | 472 | const expectedRes = { 473 | listInt: [[1, 2, 3], [1, 2, 3]], 474 | listDouble: [[1.5, 2.6, 3.9], [1.5, 2.6, 3.9]], 475 | listBoolean: [[true, false, true], [true, false, true]], 476 | listBigint: [ 477 | [BigInt('9007199254740993'), BigInt('9007199254740995'), BigInt('9007199254740997')], 478 | [BigInt('9007199254740993'), BigInt('9007199254740995'), BigInt('9007199254740997')], 479 | ], 480 | listDate: [datesWithoutTime, datesWithoutTime], 481 | listTime: [['14:25:29.425', '14:25:29.425', '14:25:29.425'], ['14:25:29.425', '14:25:29.425', '14:25:29.425']], 482 | listTimestamp: [ 483 | [ 484 | new Date('2024-10-31T14:25:29.425Z'), 485 | new Date('2024-10-30T14:25:29.425Z'), 486 | new Date('2024-10-29T14:25:29.425Z'), 487 | ], 488 | [ 489 | new Date('2024-10-31T14:25:29.425Z'), 490 | new Date('2024-10-30T14:25:29.425Z'), 491 | new Date('2024-10-29T14:25:29.425Z'), 492 | ], 493 | ], 494 | }; 495 | 496 | expect(res[0]).toStrictEqual(expectedRes); 497 | }); 498 | 499 | test('nested 3d list type test', async () => { 500 | await sql.unsafe(`create table nested_3d_list_table ( 501 | listInt integer[][][], 502 | listDouble double[][][], 503 | listBoolean boolean[][][], 504 | listBigint bigint[][][], 505 | listDate date[][][], 506 | listTime time[][][], 507 | listTimestamp timestamp[3][2][2] 508 | );`); 509 | 510 | const dates = [ 511 | new Date('2024-10-31T14:25:29.425Z'), 512 | new Date('2024-10-30T14:25:29.425Z'), 513 | new Date('2024-10-29T14:25:29.425Z'), 514 | ]; 515 | await sql.unsafe(`insert into nested_3d_list_table values ($1, $2, $3, $4, $5, $6, $7);`, [ 516 | [[[1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3]]], 517 | [[[1.5, 2.6, 3.9], [1.5, 2.6, 3.9]], [[1.5, 2.6, 3.9], [1.5, 2.6, 3.9]]], 518 | [[[true, false, true], [true, false, true]], [[true, false, true], [true, false, true]]], 519 | [ 520 | [ 521 | [ 522 | BigInt('9007199254740992') + BigInt(1), 523 | BigInt('9007199254740992') + BigInt(3), 524 | BigInt('9007199254740992') + BigInt(5), 525 | ], 526 | [ 527 | BigInt('9007199254740992') + BigInt(1), 528 | BigInt('9007199254740992') + BigInt(3), 529 | BigInt('9007199254740992') + BigInt(5), 530 | ], 531 | ], 532 | [ 533 | [ 534 | BigInt('9007199254740992') + BigInt(1), 535 | BigInt('9007199254740992') + BigInt(3), 536 | BigInt('9007199254740992') + BigInt(5), 537 | ], 538 | [ 539 | BigInt('9007199254740992') + BigInt(1), 540 | BigInt('9007199254740992') + BigInt(3), 541 | BigInt('9007199254740992') + BigInt(5), 542 | ], 543 | ], 544 | ], 545 | [[dates, dates], [dates, dates]], 546 | [[dates, dates], [dates, dates]], 547 | [[dates, dates], [dates, dates]], 548 | ]); 549 | 550 | const res = await sql.unsafe('select * from nested_3d_list_table;'); 551 | 552 | const datesWithoutTime = [...dates]; 553 | for (const date of datesWithoutTime) date.setUTCHours(0, 0, 0, 0); 554 | 555 | const expectedRes = { 556 | listInt: [[[1, 2, 3], [1, 2, 3]], [[1, 2, 3], [1, 2, 3]]], 557 | listDouble: [[[1.5, 2.6, 3.9], [1.5, 2.6, 3.9]], [[1.5, 2.6, 3.9], [1.5, 2.6, 3.9]]], 558 | listBoolean: [[[true, false, true], [true, false, true]], [[true, false, true], [true, false, true]]], 559 | listBigint: [ 560 | [ 561 | [BigInt('9007199254740993'), BigInt('9007199254740995'), BigInt('9007199254740997')], 562 | [BigInt('9007199254740993'), BigInt('9007199254740995'), BigInt('9007199254740997')], 563 | ], 564 | [ 565 | [BigInt('9007199254740993'), BigInt('9007199254740995'), BigInt('9007199254740997')], 566 | [BigInt('9007199254740993'), BigInt('9007199254740995'), BigInt('9007199254740997')], 567 | ], 568 | ], 569 | listDate: [[datesWithoutTime, datesWithoutTime], [datesWithoutTime, datesWithoutTime]], 570 | listTime: [ 571 | [ 572 | ['14:25:29.425', '14:25:29.425', '14:25:29.425'], 573 | ['14:25:29.425', '14:25:29.425', '14:25:29.425'], 574 | ], 575 | [ 576 | ['14:25:29.425', '14:25:29.425', '14:25:29.425'], 577 | ['14:25:29.425', '14:25:29.425', '14:25:29.425'], 578 | ], 579 | ], 580 | listTimestamp: [ 581 | [ 582 | [ 583 | new Date('2024-10-31T14:25:29.425Z'), 584 | new Date('2024-10-30T14:25:29.425Z'), 585 | new Date('2024-10-29T14:25:29.425Z'), 586 | ], 587 | [ 588 | new Date('2024-10-31T14:25:29.425Z'), 589 | new Date('2024-10-30T14:25:29.425Z'), 590 | new Date('2024-10-29T14:25:29.425Z'), 591 | ], 592 | ], 593 | [ 594 | [ 595 | new Date('2024-10-31T14:25:29.425Z'), 596 | new Date('2024-10-30T14:25:29.425Z'), 597 | new Date('2024-10-29T14:25:29.425Z'), 598 | ], 599 | [ 600 | new Date('2024-10-31T14:25:29.425Z'), 601 | new Date('2024-10-30T14:25:29.425Z'), 602 | new Date('2024-10-29T14:25:29.425Z'), 603 | ], 604 | ], 605 | ], 606 | }; 607 | 608 | expect(res[0]).toStrictEqual(expectedRes); 609 | }); 610 | 611 | // sql template 612 | test('sql template types test', async () => { 613 | await sql` 614 | create table sql_template_table ( 615 | smallint_ smallint, 616 | integer_ integer, 617 | bigint_ bigint, 618 | double_ double, 619 | varchar_ varchar, 620 | boolean_ boolean, 621 | time_ time, 622 | date_ date, 623 | timestamp_ timestamp, 624 | json_ json, 625 | arrayInt integer[3], 626 | listInt integer[], 627 | arrayBigint bigint[1], 628 | listBigint bigint[], 629 | arrayBoolean boolean[3], 630 | listBoolean boolean[], 631 | arrayDouble double[3], 632 | listDouble double[], 633 | arrayJson json[1], 634 | listJson json[], 635 | arrayVarchar varchar[3], 636 | listVarchar varchar[], 637 | arrayTime time[3], 638 | listTime time[], 639 | arrayDate date[3], 640 | listDate date[], 641 | arrayTimestamp timestamp[3], 642 | listTimestamp timestamp[] 643 | ); 644 | `; 645 | 646 | const date = new Date('2024-10-31T14:25:29.425Z'); 647 | const dates = [ 648 | new Date('2024-10-31T14:25:29.425Z'), 649 | new Date('2024-10-30T14:25:29.425Z'), 650 | new Date('2024-10-29T14:25:29.425Z'), 651 | ]; 652 | 653 | await sql` 654 | insert into sql_template_table values ( 655 | ${1}, ${10}, ${BigInt('9007199254740992') + BigInt(1)}, 656 | ${20.4}, ${'qwerty'}, ${true}, ${date}, ${date}, ${date}, 657 | ${{ 658 | name: 'alex', 659 | age: 26, 660 | bookIds: [1, 2, 3], 661 | vacationRate: 2.5, 662 | aliases: ['sasha', 'sanya'], 663 | isMarried: true, 664 | }}, 665 | ${[1, 2, 3]}, ${[1, 2, 3, 4, 5]}, 666 | ${[BigInt('9007199254740992') + BigInt(1)]}, 667 | ${[BigInt('9007199254740992') + BigInt(1), BigInt('9007199254740992') + BigInt(3)]}, 668 | ${[true, false, true]}, 669 | ${[true, false]}, 670 | ${[3.4, 52.6, 3.5]}, 671 | ${[3.4, 52.6, 3.5]}, 672 | ${[{ 673 | name: 'alex', 674 | age: 26, 675 | bookIds: [1, 2, 3], 676 | vacationRate: 2.5, 677 | aliases: ['sasha', 'sanya'], 678 | isMarried: true, 679 | }]}, 680 | ${[{ 681 | name: 'alex', 682 | age: 26, 683 | bookIds: [1, 2, 3], 684 | vacationRate: 2.5, 685 | aliases: ['sasha', 'sanya'], 686 | isMarried: true, 687 | }]}, 688 | ['hel,lo', 'world', '!'], 689 | ['hel,lo', 'world!'], 690 | ${dates}, 691 | ${dates}, 692 | ${dates}, 693 | ${dates}, 694 | ${dates}, 695 | ${dates} 696 | ); 697 | `; 698 | 699 | const res = await sql`select * from sql_template_table;`; 700 | 701 | const dateWithoutTime = new Date(date); 702 | dateWithoutTime.setUTCHours(0, 0, 0, 0); 703 | 704 | const datesWithoutTime = [...dates]; 705 | for (const date of datesWithoutTime) date.setUTCHours(0, 0, 0, 0); 706 | 707 | const expectedRes = { 708 | smallint_: 1, 709 | integer_: 10, 710 | bigint_: BigInt('9007199254740993'), 711 | double_: 20.4, 712 | varchar_: 'qwerty', 713 | boolean_: true, 714 | time_: '14:25:29.425', 715 | date_: dateWithoutTime, 716 | timestamp_: date, 717 | json_: { 718 | name: 'alex', 719 | age: 26, 720 | bookIds: [1, 2, 3], 721 | vacationRate: 2.5, 722 | aliases: ['sasha', 'sanya'], 723 | isMarried: true, 724 | }, 725 | arrayInt: [1, 2, 3], 726 | listInt: [1, 2, 3, 4, 5], 727 | arrayBigint: [BigInt('9007199254740992') + BigInt(1)], 728 | listBigint: [BigInt('9007199254740992') + BigInt(1), BigInt('9007199254740992') + BigInt(3)], 729 | arrayBoolean: [true, false, true], 730 | listBoolean: [true, false], 731 | arrayDouble: [3.4, 52.6, 3.5], 732 | listDouble: [3.4, 52.6, 3.5], 733 | arrayJson: [{ 734 | name: 'alex', 735 | age: 26, 736 | bookIds: [1, 2, 3], 737 | vacationRate: 2.5, 738 | aliases: ['sasha', 'sanya'], 739 | isMarried: true, 740 | }], 741 | listJson: [{ 742 | name: 'alex', 743 | age: 26, 744 | bookIds: [1, 2, 3], 745 | vacationRate: 2.5, 746 | aliases: ['sasha', 'sanya'], 747 | isMarried: true, 748 | }], 749 | arrayVarchar: ['hel,lo', 'world', '!'], 750 | listVarchar: ['hel,lo', 'world!'], 751 | arrayTime: ['14:25:29.425', '14:25:29.425', '14:25:29.425'], 752 | arrayDate: datesWithoutTime, 753 | arrayTimestamp: [ 754 | new Date('2024-10-31T14:25:29.425Z'), 755 | new Date('2024-10-30T14:25:29.425Z'), 756 | new Date('2024-10-29T14:25:29.425Z'), 757 | ], 758 | listTime: ['14:25:29.425', '14:25:29.425', '14:25:29.425'], 759 | listDate: datesWithoutTime, 760 | listTimestamp: [ 761 | new Date('2024-10-31T14:25:29.425Z'), 762 | new Date('2024-10-30T14:25:29.425Z'), 763 | new Date('2024-10-29T14:25:29.425Z'), 764 | ], 765 | }; 766 | expect(res[0]).toStrictEqual(expectedRes); 767 | }); 768 | -------------------------------------------------------------------------------- /tests/recycling-pool.test.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | import { expect, test } from 'vitest'; 3 | import { createRecyclingPool } from '../src/recycling-pool'; 4 | 5 | const sleep = (ms: number) => { 6 | return new Promise( 7 | (resolve) => setTimeout(resolve, ms), 8 | ); 9 | }; 10 | 11 | class Connection { 12 | public connectionId: string; 13 | constructor() { 14 | this.connectionId = crypto.randomUUID(); 15 | } 16 | } 17 | 18 | test('basic pool test', async () => { 19 | const pool = createRecyclingPool( 20 | { 21 | create: async () => { 22 | return { connection: 'connection' }; 23 | }, 24 | destroy: async () => {}, 25 | }, 26 | { 27 | recycleTimeout: 300, 28 | recycleJitter: 100, 29 | min: 0, 30 | max: 1, 31 | }, 32 | ); 33 | 34 | const connObj = await pool.acquire(); 35 | expect(connObj.connection).toBe('connection'); 36 | await pool.release(connObj); 37 | }); 38 | 39 | test('ensure minimum pool size test', async () => { 40 | // pool should not destroy connections if min pool size equals max pool size 41 | // -------------------------------------------------------------------------------------------- 42 | let pool = createRecyclingPool( 43 | { 44 | create: async () => { 45 | return new Connection(); 46 | }, 47 | destroy: async () => {}, 48 | }, 49 | { 50 | recycleTimeout: 300, 51 | recycleJitter: 100, 52 | min: 1, 53 | max: 1, 54 | }, 55 | ); 56 | 57 | let connObj = await pool.acquire(); 58 | const connIdPrev = connObj.connectionId; 59 | await sleep(500); 60 | await pool.release(connObj); 61 | 62 | connObj = await pool.acquire(); 63 | const connIdNext = connObj.connectionId; 64 | await pool.release(connObj); 65 | 66 | expect(connIdPrev).equal(connIdNext); 67 | 68 | // -------------------------------------------------------------------------------------------- 69 | const minConnNumber = 8; 70 | pool = createRecyclingPool( 71 | { 72 | create: async () => { 73 | return new Connection(); 74 | }, 75 | destroy: async () => {}, 76 | }, 77 | { 78 | recycleTimeout: 300, 79 | recycleJitter: 100, 80 | min: minConnNumber, 81 | max: minConnNumber, 82 | }, 83 | ); 84 | 85 | const connObjListPrev = []; 86 | const connIdsPrev = []; 87 | for (let i = 0; i < minConnNumber; i++) { 88 | connObj = await pool.acquire(); 89 | connObjListPrev.push(connObj); 90 | connIdsPrev.push(connObj.connectionId); 91 | } 92 | await sleep(500); 93 | for (const connObj of connObjListPrev) { 94 | await pool.release(connObj); 95 | } 96 | 97 | const connObjListNext = []; 98 | const connIdsNext: string[] = []; 99 | for (let i = 0; i < minConnNumber; i++) { 100 | connObj = await pool.acquire(); 101 | connObjListNext.push(connObj); 102 | connIdsNext.push(connObj.connectionId); 103 | } 104 | for (const connObj of connObjListNext) { 105 | await pool.release(connObj); 106 | } 107 | 108 | expect( 109 | connIdsPrev.every((connId) => connIdsNext.includes(connId)), 110 | ).toBe(true); 111 | 112 | // pool will create min number of connections after first acquire 113 | // -------------------------------------------------------------------------------------------- 114 | pool = createRecyclingPool( 115 | { 116 | create: async () => { 117 | return new Connection(); 118 | }, 119 | destroy: async () => {}, 120 | }, 121 | { 122 | recycleTimeout: 300, 123 | recycleJitter: 100, 124 | min: 4, 125 | max: 8, 126 | }, 127 | ); 128 | 129 | expect(pool.size).toBe(0); 130 | 131 | connObj = await pool.acquire(); 132 | 133 | expect(pool.size).toBe(4); 134 | 135 | await pool.release(connObj); 136 | }); 137 | 138 | test('try catch error test', async () => { 139 | const pool = createRecyclingPool( 140 | { 141 | create: async () => { 142 | throw new Error('create error'); 143 | }, 144 | destroy: async () => {}, 145 | }, 146 | { 147 | recycleTimeout: 300, 148 | recycleJitter: 100, 149 | min: 4, 150 | max: 8, 151 | }, 152 | ); 153 | 154 | await expect(pool.acquire()).rejects.toThrow('create error'); 155 | }); 156 | 157 | test('recycle timeout test', async () => { 158 | let pool = createRecyclingPool( 159 | { 160 | create: async () => { 161 | return new Connection(); 162 | }, 163 | destroy: async () => {}, 164 | }, 165 | { 166 | recycleTimeout: 300, 167 | recycleJitter: 100, 168 | min: 0, 169 | max: 1, 170 | }, 171 | ); 172 | 173 | let connObj = await pool.acquire(); 174 | const connId = connObj.connectionId; 175 | await sleep(500); 176 | await pool.release(connObj); 177 | 178 | const connObjNext = await pool.acquire(); 179 | await pool.release(connObjNext); 180 | expect(connObjNext.connectionId).not.toBe(connId); 181 | 182 | // -------------------------------------------------------------------------------------------- 183 | const maxConnNumber = 8; 184 | pool = createRecyclingPool( 185 | { 186 | create: async () => { 187 | return new Connection(); 188 | }, 189 | destroy: async () => {}, 190 | }, 191 | { 192 | recycleTimeout: 300, 193 | recycleJitter: 100, 194 | min: 0, 195 | max: maxConnNumber, 196 | }, 197 | ); 198 | 199 | const connObjListPrev = []; 200 | const connIdsPrev = []; 201 | for (let i = 0; i < maxConnNumber; i++) { 202 | connObj = await pool.acquire(); 203 | connObjListPrev.push(connObj); 204 | connIdsPrev.push(connObj.connectionId); 205 | } 206 | await sleep(500); 207 | for (const connObj of connObjListPrev) { 208 | await pool.release(connObj); 209 | } 210 | 211 | const connObjListNext = []; 212 | const connIdsNext: string[] = []; 213 | for (let i = 0; i < maxConnNumber; i++) { 214 | connObj = await pool.acquire(); 215 | connObjListNext.push(connObj); 216 | connIdsNext.push(connObj.connectionId); 217 | } 218 | for (const connObj of connObjListNext) { 219 | await pool.release(connObj); 220 | } 221 | 222 | expect( 223 | connIdsPrev.every((connId) => !connIdsNext.includes(connId)), 224 | ).toBe(true); 225 | }); 226 | 227 | test('work after error', async () => { 228 | const factory = { 229 | create: async () => { 230 | return new Connection(); 231 | }, 232 | destroy: async () => {}, 233 | }; 234 | 235 | const pool = createRecyclingPool( 236 | factory, 237 | { 238 | recycleTimeout: 300, 239 | recycleJitter: 100, 240 | min: 0, 241 | max: 2, 242 | }, 243 | ); 244 | 245 | const connObj = await pool.acquire(); 246 | expect(connObj.connectionId).toBeDefined(); 247 | 248 | factory.create = async () => { 249 | throw new Error('create error'); 250 | }; 251 | 252 | await expect(pool.acquire()).rejects.toThrow('create error'); 253 | 254 | factory.create = async () => { 255 | return new Connection(); 256 | }; 257 | 258 | const connObj2 = await pool.acquire(); 259 | expect(connObj2.connectionId).toBeDefined(); 260 | await pool.release(connObj); 261 | await pool.release(connObj2); 262 | }); 263 | -------------------------------------------------------------------------------- /tests/waddler-errors.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest'; 2 | import { waddler } from '../src/neo.ts'; 3 | 4 | test('error during connection test', async () => { 5 | const sql = waddler({ url: 'md:?motherduck_token=test', max: 10, accessMode: 'read_write' }); 6 | 7 | // sql template case 8 | await expect(sql`select 1;`).rejects.toThrow( 9 | /^Invalid Input Error: Initialization function "motherduck_init"../, 10 | ); 11 | 12 | // sql.unsafe case 13 | await expect(sql.unsafe('select 1;')).rejects.toThrow( 14 | /^Invalid Input Error: Initialization function "motherduck_init"../, 15 | ); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/waddler-unit.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, expect, test } from 'vitest'; 2 | import { waddler } from '../src/index.ts'; 3 | 4 | let sql: ReturnType; 5 | beforeAll(async () => { 6 | sql = waddler({ url: ':memory:', max: 10, accessMode: 'read_write' }); 7 | }); 8 | 9 | // toSQL 10 | test('base test', () => { 11 | const res = sql`select 1;`.toSQL(); 12 | 13 | expect(res).toStrictEqual({ query: `select 1;`, params: [] }); 14 | }); 15 | 16 | test('base test with number param', () => { 17 | const res = sql`select ${1};`.toSQL(); 18 | 19 | expect(res).toStrictEqual({ query: `select $1;`, params: [1] }); 20 | }); 21 | 22 | test('base test with bigint param', () => { 23 | const res = sql`select ${BigInt(10)};`.toSQL(); 24 | 25 | expect(res).toStrictEqual({ query: `select $1;`, params: [10n] }); 26 | }); 27 | 28 | test('base test with string param', () => { 29 | const res = sql`select ${'hello world.'};`.toSQL(); 30 | 31 | expect(res).toStrictEqual({ query: `select $1;`, params: ['hello world.'] }); 32 | }); 33 | 34 | test('base test with boolean param', () => { 35 | const res = sql`select ${true};`.toSQL(); 36 | 37 | expect(res).toStrictEqual({ query: `select $1;`, params: [true] }); 38 | }); 39 | 40 | test('base test with Date param', () => { 41 | const res = sql`select ${new Date('10.04.2025')};`.toSQL(); 42 | 43 | expect(res).toStrictEqual({ query: `select $1;`, params: [new Date('10.04.2025')] }); 44 | }); 45 | 46 | test('base test with null param', () => { 47 | const res = sql`select ${null};`.toSQL(); 48 | 49 | expect(res).toStrictEqual({ query: `select $1;`, params: [null] }); 50 | }); 51 | 52 | // errors 53 | test('base test with undefined param. error', () => { 54 | // @ts-ignore 55 | expect(() => sql`select ${undefined};`.toSQL()) 56 | .toThrowError("you can't specify undefined as parameter"); 57 | }); 58 | 59 | test('base test with symbol param. error', () => { 60 | // @ts-ignore 61 | expect(() => sql`select ${Symbol('fooo')};`.toSQL()) 62 | .toThrowError("you can't specify symbol as parameter"); 63 | }); 64 | 65 | test('base test with function param. error', () => { 66 | // @ts-ignore 67 | expect(() => sql`select ${() => {}};`.toSQL()) 68 | .toThrowError("you can't specify function as parameter"); 69 | }); 70 | 71 | // identifier ---------------------------------------------------------------------------------- 72 | test('sql.identifier test. string parameter', () => { 73 | const res = sql`select ${sql.identifier('name')} from users;`.toSQL(); 74 | 75 | expect(res).toStrictEqual({ query: `select "name" from users;`, params: [] }); 76 | }); 77 | 78 | test('sql.identifier test. string[] parameter', () => { 79 | const res = sql`select ${sql.identifier(['name', 'email', 'phone'])} from users;`.toSQL(); 80 | 81 | expect(res).toStrictEqual({ query: `select "name", "email", "phone" from users;`, params: [] }); 82 | }); 83 | 84 | test('sql.identifier test. object parameter', () => { 85 | const res = sql`select * from ${sql.identifier({ schema: 'public', table: 'users' })};`.toSQL(); 86 | 87 | expect(res).toStrictEqual({ query: `select * from "public"."users";`, params: [] }); 88 | }); 89 | 90 | test('sql.identifier test. object[] parameter', () => { 91 | const res = sql`select ${ 92 | sql.identifier([ 93 | { schema: 'public', table: 'users', column: 'name' }, 94 | { schema: 'public', table: 'users', column: 'email' }, 95 | ]) 96 | } from users;`.toSQL(); 97 | 98 | expect(res).toStrictEqual({ 99 | query: `select "public"."users"."name", "public"."users"."email" from users;`, 100 | params: [], 101 | }); 102 | }); 103 | 104 | test('sql.identifier test. object[] parameter', () => { 105 | const res = sql`select ${ 106 | sql.identifier([ 107 | { schema: 'public', table: 'users', column: 'name', as: 'user_name' }, 108 | { schema: 'public', table: 'users', column: 'email', as: 'user_email' }, 109 | ]) 110 | } from users;`.toSQL(); 111 | 112 | expect(res).toStrictEqual({ 113 | query: `select "public"."users"."name" as "user_name", "public"."users"."email" as "user_email" from users;`, 114 | params: [], 115 | }); 116 | }); 117 | 118 | // errors 119 | test('sql.identifier test. undefined | number | bigint | boolean | symbol | function | null as parameter. error', () => { 120 | const paramList = [undefined, 1, BigInt(10), true, Symbol('fooo'), () => {}]; 121 | for (const param of paramList) { 122 | expect( 123 | // @ts-ignore 124 | () => sql`select ${sql.identifier(param)} from users;`.toSQL(), 125 | ).toThrowError(`you can't specify ${typeof param} as parameter for sql.identifier.`); 126 | } 127 | 128 | expect( 129 | // @ts-ignore 130 | () => sql`select ${sql.identifier(null)} from users;`.toSQL(), 131 | ).toThrowError(`you can't specify null as parameter for sql.identifier.`); 132 | }); 133 | 134 | test('sql.identifier test. array of undefined | number | bigint | boolean | symbol | function | null | array as parameter. error', () => { 135 | const paramList = [ 136 | ['name', undefined], 137 | ['name', 1], 138 | ['name', BigInt(10)], 139 | ['name', true], 140 | ['name', Symbol('foo')], 141 | ['name', () => {}], 142 | ]; 143 | for (const param of paramList) { 144 | expect( 145 | // @ts-ignore 146 | () => sql`select ${sql.identifier(param)} from users;`.toSQL(), 147 | ).toThrowError( 148 | `you can't specify array of (null or undefined or number or bigint or boolean or symbol or function) as parameter for sql.identifier.`, 149 | ); 150 | } 151 | 152 | expect( 153 | // @ts-ignore 154 | () => sql`select ${sql.identifier([null])} from users;`.toSQL(), 155 | ).toThrowError( 156 | `you can't specify array of (null or undefined or number or bigint or boolean or symbol or function) as parameter for sql.identifier.`, 157 | ); 158 | 159 | expect( 160 | // @ts-ignore 161 | () => sql`select ${sql.identifier(['name', []])} from users;`.toSQL(), 162 | ).toThrowError(`you can't specify array of arrays as parameter for sql.identifier.`); 163 | }); 164 | 165 | test("sql.identifier test. 'empty array' error", () => { 166 | expect( 167 | () => sql`select ${sql.identifier([])} from users;`.toSQL(), 168 | ).toThrowError(`you can't specify empty array as parameter for sql.identifier.`); 169 | }); 170 | 171 | test("sql.identifier test. 'undefined in parameters' error with object parameter", () => { 172 | expect( 173 | () => sql`select ${sql.identifier({ schema: undefined })}, "email", "phone" from "public"."users";`.toSQL(), 174 | ).toThrowError(`you can't specify undefined parameters. maybe you want to omit it?`); 175 | }); 176 | 177 | test("sql.identifier test. 'no parameters' error with object parameter", () => { 178 | expect( 179 | () => sql`select ${sql.identifier({})}, "email", "phone" from "public"."users";`.toSQL(), 180 | ).toThrowError(`you need to specify at least one parameter.`); 181 | }); 182 | 183 | test("sql.identifier test. 'only schema and column' error with object parameter", () => { 184 | expect( 185 | () => 186 | sql`select ${sql.identifier({ schema: 'public', column: 'name' })}, "email", "phone" from "public"."users";` 187 | .toSQL(), 188 | ).toThrowError(`you can't specify only "schema" and "column" properties, you need also specify "table".`); 189 | }); 190 | 191 | test("sql.identifier test. 'only as' error with object parameter", () => { 192 | expect( 193 | () => sql`select ${sql.identifier({ as: 'user_name' })}, "email", "phone" from "public"."users";`.toSQL(), 194 | ).toThrowError(`you can't specify only "as" property.`); 195 | }); 196 | 197 | test("sql.identifier test. 'column or table should be specified with as' error with object parameter", () => { 198 | expect( 199 | () => 200 | sql`select ${sql.identifier({ schema: 'public', as: 'user_name' })}, "email", "phone" from "public"."users";` 201 | .toSQL(), 202 | ).toThrowError(`you have to specify "column" or "table" property along with "as".`); 203 | }); 204 | 205 | test("sql.identifier test. wrong types in object's properties 'schema', 'table', 'column', 'as'. error with object parameter", () => { 206 | expect( 207 | () => 208 | sql`select ${ 209 | // @ts-ignore 210 | sql.identifier({ 211 | schema: 'public', 212 | table: 'users', 213 | column: 'name', 214 | as: 4, 215 | })}, "email", "phone" from "public"."users";`.toSQL(), 216 | ).toThrowError(`object properties 'schema', 'table', 'column', 'as' should be of string type or omitted.`); 217 | 218 | expect( 219 | // @ts-ignore 220 | () => 221 | sql`select ${ 222 | // @ts-ignore 223 | sql.identifier({ 224 | schema: 'public', 225 | table: 'users', 226 | column: 4, 227 | as: 'user_name', 228 | })}, "email", "phone" from "public"."users";`.toSQL(), 229 | ).toThrowError(`object properties 'schema', 'table', 'column', 'as' should be of string type or omitted.`); 230 | 231 | expect( 232 | // @ts-ignore 233 | () => 234 | sql`select ${ 235 | // @ts-ignore 236 | sql.identifier({ 237 | schema: 'public', 238 | table: 4, 239 | column: 'name', 240 | as: 'user_name', 241 | })}, "email", "phone" from "public"."users";`.toSQL(), 242 | ).toThrowError(`object properties 'schema', 'table', 'column', 'as' should be of string type or omitted.`); 243 | 244 | expect( 245 | // @ts-ignore 246 | () => 247 | sql`select ${ 248 | // @ts-ignore 249 | sql.identifier({ 250 | schema: 4, 251 | table: 'users', 252 | column: 'name', 253 | as: 'user_name', 254 | })}, "email", "phone" from "public"."users";`.toSQL(), 255 | ).toThrowError(`object properties 'schema', 'table', 'column', 'as' should be of string type or omitted.`); 256 | }); 257 | 258 | // values ---------------------------------------------------------------------------------- 259 | test('sql.values test. ', () => { 260 | const res = sql`insert into users (id, name, is_active) values ${ 261 | sql.values([[1, 'Oleksii', false], [2, 'Alex', true]]) 262 | };`.toSQL(); 263 | expect(res).toStrictEqual({ 264 | query: "insert into users (id, name, is_active) values (1, 'Oleksii', false), (2, 'Alex', true);", 265 | params: [], 266 | }); 267 | }); 268 | 269 | test('sql.values test. number, boolean, string, bigint, null, Date, SQLDefault as values', () => { 270 | const res = sql`insert into users (id, is_active, name, bigint_, null_) values ${ 271 | sql.values([[1, true, 'Oleksii', BigInt(1), null, new Date('10.04.2025'), sql.default]]) 272 | };`.toSQL(); 273 | expect(res).toStrictEqual({ 274 | query: 275 | "insert into users (id, is_active, name, bigint_, null_) values (1, true, 'Oleksii', 1, null, '2025-10-03T21:00:00.000Z', default);", 276 | params: [], 277 | }); 278 | }); 279 | 280 | test('sql.values array type test', async () => { 281 | const dates = [ 282 | new Date('2024-10-31T14:25:29.425Z'), 283 | new Date('2024-10-30T14:25:29.425Z'), 284 | new Date('2024-10-29T14:25:29.425Z'), 285 | ]; 286 | const query = sql`insert into array_table values ${ 287 | sql.values([[ 288 | [1, 2, 3], 289 | [1.5, 2.6, 3.9], 290 | [true, false, true], 291 | [ 292 | BigInt('9007199254740992') + BigInt(1), 293 | BigInt('9007199254740992') + BigInt(3), 294 | BigInt('9007199254740992') + BigInt(5), 295 | ], 296 | dates, 297 | ]]) 298 | };`; 299 | 300 | const expectedQuery = 'insert into array_table values (' 301 | + '[1,2,3], [1.5,2.6,3.9], [true,false,true], [9007199254740993,9007199254740995,9007199254740997], ' 302 | + "['2024-10-31T14:25:29.425Z','2024-10-30T14:25:29.425Z','2024-10-29T14:25:29.425Z']);"; 303 | 304 | expect(query.toSQL().query).toStrictEqual(expectedQuery); 305 | }); 306 | 307 | // errors 308 | test('sql.values test. undefined | string | number | object |bigint | boolean | symbol | function | null as parameter. error', () => { 309 | const paramList = [undefined, 'hello world', 1, {}, BigInt(10), true, Symbol('fooo'), () => {}]; 310 | for (const param of paramList) { 311 | expect( 312 | // @ts-ignore 313 | () => sql`insert into users (id, name, is_active) values ${sql.values(param)};`.toSQL(), 314 | ).toThrowError(`you can't specify ${typeof param} as parameter for sql.values.`); 315 | } 316 | 317 | expect( 318 | // @ts-ignore 319 | () => sql`insert into users (id, name, is_active) values ${sql.values(null)};`.toSQL(), 320 | ).toThrowError(`you can't specify null as parameter for sql.values.`); 321 | }); 322 | 323 | test("sql.values test. 'empty array' error", () => { 324 | expect( 325 | () => sql`insert into users (id, name, is_active) values ${sql.values([])};`.toSQL(), 326 | ).toThrowError(`you can't specify empty array as parameter for sql.values.`); 327 | }); 328 | 329 | test('sql.values test. array of null | undefined | object | number | bigint | boolean | function | symbol | string as parameter. error', () => { 330 | expect( 331 | // @ts-ignore 332 | () => sql`insert into users (id, name, is_active) values ${sql.values([null])};`.toSQL(), 333 | ).toThrowError(`you can't specify array of null as parameter for sql.values.`); 334 | 335 | const valsList = [undefined, {}, 1, BigInt(10), true, () => {}, Symbol('fooo'), 'fooo']; 336 | for (const val of valsList) { 337 | expect( 338 | // @ts-ignore 339 | () => sql`insert into users (id, name, is_active) values ${sql.values([val])};`.toSQL(), 340 | ).toThrowError(`you can't specify array of ${typeof val} as parameter for sql.values.`); 341 | } 342 | }); 343 | 344 | test('sql.values test. array of empty array. error', () => { 345 | expect( 346 | // @ts-ignore 347 | () => sql`insert into users (id, name, is_active) values ${sql.values([[]])};`.toSQL(), 348 | ).toThrowError(`array of values can't be empty.`); 349 | }); 350 | 351 | test('sql.values test. array | object | undefined | symbol | function as value. error', () => { 352 | let valsList = [{}]; 353 | 354 | for (const val of valsList) { 355 | expect( 356 | // @ts-ignore 357 | () => sql`insert into users (id, name, is_active) values ${sql.values([[val]])};`.toSQL(), 358 | ).toThrowError( 359 | `value can't be object. you can't specify [ [ {...}, ...], ...] as parameter for sql.values.`, 360 | ); 361 | } 362 | 363 | expect( 364 | // @ts-ignore 365 | () => sql`insert into users (id, name, is_active) values ${sql.values([[undefined]])};`.toSQL(), 366 | ).toThrowError(`value can't be undefined, maybe you mean sql.default?`); 367 | 368 | valsList = [Symbol('fooo'), () => {}]; 369 | for (const val of valsList) { 370 | expect( 371 | // @ts-ignore 372 | () => sql`insert into users (id, name, is_active) values ${sql.values([[val]])};`.toSQL(), 373 | ).toThrowError(`you can't specify ${typeof val} as value.`); 374 | } 375 | }); 376 | 377 | // raw ---------------------------------------------------------------------------------- 378 | test('sql.raw test. number | boolean | bigint | string as parameter.', () => { 379 | let res = sql`select ${sql.raw(1)};`.toSQL(); 380 | expect(res).toStrictEqual({ query: 'select 1;', params: [] }); 381 | 382 | res = sql`select ${sql.raw(true)};`.toSQL(); 383 | expect(res).toStrictEqual({ query: 'select true;', params: [] }); 384 | 385 | res = sql`select ${sql.raw(BigInt(10))};`.toSQL(); 386 | expect(res).toStrictEqual({ query: 'select 10;', params: [] }); 387 | 388 | res = sql`select ${sql.raw('* from users')};`.toSQL(); 389 | expect(res).toStrictEqual({ query: 'select * from users;', params: [] }); 390 | }); 391 | 392 | // errors 393 | test('sql.raw test. array | object | null | undefined | symbol | function as parameter. error.', () => { 394 | let paramList = [[], {}, null]; 395 | for (const param of paramList) { 396 | expect( 397 | // @ts-ignore 398 | () => sql`select ${sql.raw(param)};`.toSQL(), 399 | ).toThrowError(`you can't specify array, object or null as parameter for sql.raw.`); 400 | } 401 | 402 | expect( 403 | // @ts-ignore 404 | () => sql`select ${sql.raw(undefined)};`.toSQL(), 405 | ).toThrowError(`you can't specify undefined as parameter for sql.raw, maybe you mean using sql.default?`); 406 | 407 | paramList = [Symbol('fooo'), () => {}]; 408 | for (const param of paramList) { 409 | expect( 410 | // @ts-ignore 411 | () => sql`select ${sql.raw(param)};`.toSQL(), 412 | ).toThrowError(`you can't specify ${typeof param} as parameter for sql.raw.`); 413 | } 414 | }); 415 | 416 | // default ------------------------------------------------------------------------------ 417 | test('sql.default test using with sql.values.', () => { 418 | const res = sql`insert into users (id, name) values ${sql.values([[sql.default, 'name1']])};`.toSQL(); 419 | expect(res).toStrictEqual({ query: "insert into users (id, name) values (default, 'name1');", params: [] }); 420 | }); 421 | 422 | test('sql.default test using with sql`${}` as parameter.', () => { 423 | const res = sql`insert into users (id, name) values (${sql.default}, 'name1');`.toSQL(); 424 | expect(res).toStrictEqual({ query: "insert into users (id, name) values (default, 'name1');", params: [] }); 425 | }); 426 | 427 | // sql template 428 | test('sql template types test', async () => { 429 | await sql` 430 | create table sql_template_table ( 431 | smallint_ smallint, 432 | integer_ integer, 433 | bigint_ bigint, 434 | double_ double, 435 | varchar_ varchar, 436 | boolean_ boolean, 437 | time_ time, 438 | date_ date, 439 | timestamp_ timestamp, 440 | json_ json, 441 | arrayInt integer[3], 442 | listInt integer[], 443 | listBigint bigint[], 444 | arrayBoolean boolean[3], 445 | listBoolean boolean[], 446 | arrayDouble double[3], 447 | listDouble double[], 448 | arrayJson json[1], 449 | listJson json[], 450 | arrayVarchar varchar[3], 451 | listVarchar varchar[], 452 | arrayTime time[3], 453 | listTime time[], 454 | arrayDate date[3], 455 | listDate date[], 456 | arrayTimestamp timestamp[3], 457 | listTimestamp timestamp[] 458 | ); 459 | `; 460 | 461 | const date = new Date('2024-10-31T14:25:29.425Z'); 462 | const dates = [ 463 | new Date('2024-10-31T14:25:29.425Z'), 464 | new Date('2024-10-30T14:25:29.425Z'), 465 | new Date('2024-10-29T14:25:29.425Z'), 466 | ]; 467 | const query = sql` 468 | insert into sql_template_table values ( 469 | ${1}, ${10}, ${BigInt('9007199254740992') + BigInt(1)}, 470 | ${20.4}, ${'qwerty'}, ${true}, ${date}, ${date}, ${date}, 471 | ${{ 472 | name: 'alex', 473 | age: 26, 474 | bookIds: [1, 2, 3], 475 | vacationRate: 2.5, 476 | aliases: ['sasha', 'sanya'], 477 | isMarried: true, 478 | }}, 479 | ${[1, 2, 3]}, ${[1, 2, 3, 4, 5]}, 480 | ${[BigInt('9007199254740992') + BigInt(1), BigInt('9007199254740992') + BigInt(3)]}, 481 | ${[true, false, true]}, 482 | ${[true, false]}, 483 | ${[3.4, 52.6, 3.5]}, 484 | ${[3.4, 52.6, 3.5]}, 485 | ${[{ 486 | name: 'alex', 487 | age: 26, 488 | bookIds: [1, 2, 3], 489 | vacationRate: 2.5, 490 | aliases: ['sasha', 'sanya'], 491 | isMarried: true, 492 | }]}, 493 | ${[{ 494 | name: 'alex', 495 | age: 26, 496 | bookIds: [1, 2, 3], 497 | vacationRate: 2.5, 498 | aliases: ['sasha', 'sanya'], 499 | isMarried: true, 500 | }]}, 501 | ['hel,lo', 'world', '!'], 502 | ['hel,lo', 'world', '!'], 503 | ${dates}, 504 | ${dates}, 505 | ${dates}, 506 | ${dates}, 507 | ${dates}, 508 | ${dates} 509 | ); 510 | `; 511 | 512 | await query; 513 | 514 | const res = await sql`select * from sql_template_table;`; 515 | 516 | const dateWithoutTime = new Date(date); 517 | dateWithoutTime.setUTCHours(0, 0, 0, 0); 518 | 519 | const datesWithoutTime = [...dates]; 520 | for (const date of datesWithoutTime) date.setUTCHours(0, 0, 0, 0); 521 | 522 | const expectedRes = { 523 | smallint_: 1, 524 | integer_: 10, 525 | bigint_: BigInt('9007199254740993'), 526 | double_: 20.4, 527 | varchar_: 'qwerty', 528 | boolean_: true, 529 | time_: '14:25:29.425', 530 | date_: dateWithoutTime, 531 | timestamp_: date, 532 | json_: { 533 | name: 'alex', 534 | age: 26, 535 | bookIds: [1, 2, 3], 536 | vacationRate: 2.5, 537 | aliases: ['sasha', 'sanya'], 538 | isMarried: true, 539 | }, 540 | arrayInt: [1, 2, 3], 541 | listInt: [1, 2, 3, 4, 5], 542 | listBigint: [BigInt('9007199254740992') + BigInt(1), BigInt('9007199254740992') + BigInt(3)], 543 | arrayBoolean: [true, false, true], 544 | listBoolean: [true, false], 545 | arrayDouble: [3.4, 52.6, 3.5], 546 | listDouble: [3.4, 52.6, 3.5], 547 | arrayJson: [{ 548 | name: 'alex', 549 | age: 26, 550 | bookIds: [1, 2, 3], 551 | vacationRate: 2.5, 552 | aliases: ['sasha', 'sanya'], 553 | isMarried: true, 554 | }], 555 | listJson: [{ 556 | name: 'alex', 557 | age: 26, 558 | bookIds: [1, 2, 3], 559 | vacationRate: 2.5, 560 | aliases: ['sasha', 'sanya'], 561 | isMarried: true, 562 | }], 563 | arrayVarchar: '[hel,lo, world, !]', 564 | listVarchar: ['hel,lo', 'world', '!'], 565 | arrayTime: '[14:25:29.425, 14:25:29.425, 14:25:29.425]', 566 | arrayDate: '[2024-10-31, 2024-10-30, 2024-10-29]', 567 | arrayTimestamp: '[2024-10-31 14:25:29.425, 2024-10-30 14:25:29.425, 2024-10-29 14:25:29.425]', 568 | listTime: ['14:25:29.425', '14:25:29.425', '14:25:29.425'], 569 | listDate: datesWithoutTime, 570 | listTimestamp: [ 571 | new Date('2024-10-31T14:25:29.425Z'), 572 | new Date('2024-10-30T14:25:29.425Z'), 573 | new Date('2024-10-29T14:25:29.425Z'), 574 | ], 575 | }; 576 | expect(res[0]).toStrictEqual(expectedRes); 577 | }); 578 | 579 | // sql.append 580 | test('sql.append test.', async () => { 581 | const query = sql`select * from users where id = ${1}`; 582 | 583 | query.append(sql` or id = ${3}`); 584 | query.append(sql` or id = ${4};`); 585 | 586 | const res = query.toSQL(); 587 | expect(res).toStrictEqual({ 588 | query: 'select * from users where id = $1 or id = $2 or id = $3;', 589 | params: [1, 3, 4], 590 | }); 591 | }); 592 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "isolatedModules": true, 4 | "composite": false, 5 | "target": "esnext", 6 | "module": "esnext", 7 | "moduleResolution": "bundler", 8 | "lib": ["es2020", "es2018", "es2017", "es7", "es6", "es5", "es2022"], 9 | "declarationMap": false, 10 | "sourceMap": true, 11 | "allowJs": true, 12 | "incremental": false, 13 | "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 14 | "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 15 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 16 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 17 | "strict": true, /* Enable all strict type-checking options. */ 18 | "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 19 | "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 20 | "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 21 | "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 22 | "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 23 | "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 24 | "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 25 | "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 26 | "exactOptionalPropertyTypes": false, /* Interpret optional property types as written, rather than adding 'undefined'. */ 27 | "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 28 | "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 29 | "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 30 | "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 31 | "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 32 | "allowUnusedLabels": false, /* Disable error reporting for unused labels. */ 33 | "allowUnreachableCode": false, /* Disable error reporting for unreachable code. */ 34 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 35 | "noErrorTruncation": true, /* Disable truncating types in error messages. */ 36 | "checkJs": true, 37 | "noEmit": true, 38 | "allowImportingTsExtensions": true, 39 | "outDir": "dist", 40 | "baseUrl": ".", 41 | "declaration": true, 42 | "paths": { 43 | "~/*": ["src/*"] 44 | } 45 | }, 46 | "exclude": ["dist", "db", "src/test.ts", "src/db-test.ts"], 47 | "include": ["src", "*.ts", "scripts/build.ts", "tests"] 48 | } 49 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: [ 6 | './tests/**/*.test.ts', 7 | ], 8 | exclude: [], 9 | typecheck: { 10 | tsconfig: 'tsconfig.json', 11 | }, 12 | }, 13 | }); 14 | --------------------------------------------------------------------------------