├── .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 |
--------------------------------------------------------------------------------