turbine-orm 0.81.0 → 0.90.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -25
- package/dist/checkout.d.ts +27 -2
- package/dist/checkout.js +75 -4
- package/dist/cjs/checkout.d.ts +27 -2
- package/dist/cjs/checkout.js +76 -3
- package/dist/cjs/cli/code-first-tags.d.ts +62 -0
- package/dist/cjs/cli/code-first-tags.js +124 -0
- package/dist/cjs/cli/compile-query.js +9 -0
- package/dist/cjs/cli/config.d.ts +35 -6
- package/dist/cjs/cli/config.js +17 -1
- package/dist/cjs/cli/destructive.d.ts +7 -4
- package/dist/cjs/cli/destructive.js +224 -20
- package/dist/cjs/cli/error-catalog.js +36 -0
- package/dist/cjs/cli/error-text.d.ts +15 -0
- package/dist/cjs/cli/error-text.js +40 -0
- package/dist/cjs/cli/index.d.ts +62 -4
- package/dist/cjs/cli/index.js +568 -108
- package/dist/cjs/cli/local-server-guard.d.ts +53 -0
- package/dist/cjs/cli/local-server-guard.js +119 -0
- package/dist/cjs/cli/local-server-output.d.ts +41 -0
- package/dist/cjs/cli/local-server-output.js +103 -0
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +121 -32
- package/dist/cjs/cli/migrate.d.ts +113 -5
- package/dist/cjs/cli/migrate.js +270 -21
- package/dist/cjs/cli/observe.d.ts +6 -1
- package/dist/cjs/cli/observe.js +85 -55
- package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cjs/cli/pii-predicate-guard.js +98 -37
- package/dist/cjs/cli/prisma-resolve.js +17 -13
- package/dist/cjs/cli/prisma-schema.js +12 -4
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +14 -2
- package/dist/cjs/cli/studio.js +244 -69
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +558 -85
- package/dist/cjs/client.js +1270 -395
- package/dist/cjs/connection-guard.d.ts +13 -0
- package/dist/cjs/connection-guard.js +28 -0
- package/dist/cjs/connection-url.d.ts +51 -16
- package/dist/cjs/connection-url.js +67 -34
- package/dist/cjs/diagnostics.d.ts +81 -0
- package/dist/cjs/diagnostics.js +147 -0
- package/dist/cjs/dialect.d.ts +449 -20
- package/dist/cjs/dialect.js +117 -76
- package/dist/cjs/engine-config.d.ts +7 -6
- package/dist/cjs/errors.d.ts +126 -6
- package/dist/cjs/errors.js +256 -13
- package/dist/cjs/generate.d.ts +57 -7
- package/dist/cjs/generate.js +635 -279
- package/dist/cjs/global-filters-config.d.ts +59 -0
- package/dist/cjs/global-filters-config.js +367 -0
- package/dist/cjs/index.d.ts +9 -4
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.d.ts +31 -6
- package/dist/cjs/introspect.js +108 -50
- package/dist/cjs/log-codes.d.ts +12 -0
- package/dist/cjs/log-codes.js +77 -0
- package/dist/cjs/logger.d.ts +87 -0
- package/dist/cjs/logger.js +98 -0
- package/dist/cjs/mssql.d.ts +8 -5
- package/dist/cjs/mssql.js +352 -65
- package/dist/cjs/mysql.d.ts +3 -5
- package/dist/cjs/mysql.js +420 -70
- package/dist/cjs/nested-write.d.ts +3 -0
- package/dist/cjs/nested-write.js +179 -114
- package/dist/cjs/pipeline-submittable.d.ts +12 -0
- package/dist/cjs/pipeline-submittable.js +118 -44
- package/dist/cjs/pipeline.js +21 -26
- package/dist/cjs/powdb-introspect.d.ts +5 -2
- package/dist/cjs/powdb-introspect.js +20 -15
- package/dist/cjs/powdb-shared.d.ts +36 -5
- package/dist/cjs/powdb-shared.js +67 -8
- package/dist/cjs/powdb.d.ts +4 -0
- package/dist/cjs/powdb.js +183 -51
- package/dist/cjs/powql.d.ts +394 -51
- package/dist/cjs/powql.js +1829 -284
- package/dist/cjs/prisma-compat.d.ts +23 -15
- package/dist/cjs/prisma-compat.js +507 -372
- package/dist/cjs/query/aggregates.d.ts +17 -1
- package/dist/cjs/query/aggregates.js +261 -29
- package/dist/cjs/query/batched-loader.d.ts +25 -6
- package/dist/cjs/query/batched-loader.js +103 -20
- package/dist/cjs/query/builder.d.ts +325 -184
- package/dist/cjs/query/builder.js +1452 -490
- package/dist/cjs/query/column-ref.d.ts +61 -0
- package/dist/cjs/query/column-ref.js +73 -0
- package/dist/cjs/query/compound-unique.d.ts +21 -0
- package/dist/cjs/query/compound-unique.js +98 -18
- package/dist/cjs/query/deferred.d.ts +53 -16
- package/dist/cjs/query/expr.d.ts +217 -0
- package/dist/cjs/query/expr.js +859 -0
- package/dist/cjs/query/filters.d.ts +155 -53
- package/dist/cjs/query/filters.js +613 -76
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/json-value.d.ts +43 -0
- package/dist/cjs/query/json-value.js +59 -0
- package/dist/cjs/query/normalize-args.d.ts +87 -0
- package/dist/cjs/query/normalize-args.js +527 -0
- package/dist/cjs/query/option-surface.d.ts +16 -4
- package/dist/cjs/query/option-surface.js +57 -14
- package/dist/cjs/query/relation-names.d.ts +14 -0
- package/dist/cjs/query/relation-names.js +77 -18
- package/dist/cjs/query/relations.d.ts +30 -3
- package/dist/cjs/query/relations.js +219 -63
- package/dist/cjs/query/row-lock.d.ts +49 -0
- package/dist/cjs/query/row-lock.js +138 -0
- package/dist/cjs/query/sql-comment.d.ts +161 -0
- package/dist/cjs/query/sql-comment.js +381 -0
- package/dist/cjs/query/stream-idle.d.ts +132 -0
- package/dist/cjs/query/stream-idle.js +212 -0
- package/dist/cjs/query/table-client.d.ts +101 -0
- package/dist/cjs/query/table-client.js +74 -0
- package/dist/cjs/query/types.d.ts +677 -298
- package/dist/cjs/query/utils.d.ts +105 -6
- package/dist/cjs/query/utils.js +426 -19
- package/dist/cjs/query/warn-registry.d.ts +18 -0
- package/dist/cjs/query/warn-registry.js +18 -0
- package/dist/cjs/query/where-compile.d.ts +138 -14
- package/dist/cjs/query/where-compile.js +513 -29
- package/dist/cjs/query/where.d.ts +159 -59
- package/dist/cjs/query/where.js +716 -246
- package/dist/cjs/query/writes.d.ts +112 -50
- package/dist/cjs/query/writes.js +1498 -200
- package/dist/cjs/query-events.d.ts +4 -2
- package/dist/cjs/query-events.js +3 -1
- package/dist/cjs/realtime.d.ts +7 -3
- package/dist/cjs/realtime.js +13 -2
- package/dist/cjs/schema-builder.d.ts +55 -4
- package/dist/cjs/schema-builder.js +382 -18
- package/dist/cjs/schema-metadata.d.ts +21 -1
- package/dist/cjs/schema-metadata.js +29 -7
- package/dist/cjs/schema-sql.d.ts +75 -5
- package/dist/cjs/schema-sql.js +497 -45
- package/dist/cjs/schema.d.ts +26 -5
- package/dist/cjs/schema.js +110 -13
- package/dist/cjs/sql-placeholders.d.ts +56 -0
- package/dist/cjs/sql-placeholders.js +204 -0
- package/dist/cjs/sqlite.d.ts +75 -20
- package/dist/cjs/sqlite.js +593 -95
- package/dist/cjs/supplied-pool-types.d.ts +48 -0
- package/dist/cjs/supplied-pool-types.js +256 -0
- package/dist/cjs/transaction-control.d.ts +51 -0
- package/dist/cjs/transaction-control.js +316 -0
- package/dist/cjs/transaction-state.d.ts +342 -0
- package/dist/cjs/transaction-state.js +707 -0
- package/dist/cjs/typed-sql.d.ts +37 -3
- package/dist/cjs/typed-sql.js +102 -8
- package/dist/cli/code-first-tags.d.ts +62 -0
- package/dist/cli/code-first-tags.js +120 -0
- package/dist/cli/compile-query.js +9 -0
- package/dist/cli/config.d.ts +35 -6
- package/dist/cli/config.js +16 -1
- package/dist/cli/destructive.d.ts +7 -4
- package/dist/cli/destructive.js +224 -20
- package/dist/cli/error-catalog.js +36 -0
- package/dist/cli/error-text.d.ts +15 -0
- package/dist/cli/error-text.js +37 -0
- package/dist/cli/index.d.ts +62 -4
- package/dist/cli/index.js +569 -112
- package/dist/cli/local-server-guard.d.ts +53 -0
- package/dist/cli/local-server-guard.js +113 -0
- package/dist/cli/local-server-output.d.ts +41 -0
- package/dist/cli/local-server-output.js +98 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +121 -34
- package/dist/cli/migrate.d.ts +113 -5
- package/dist/cli/migrate.js +261 -21
- package/dist/cli/observe.d.ts +6 -1
- package/dist/cli/observe.js +84 -54
- package/dist/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cli/pii-predicate-guard.js +100 -35
- package/dist/cli/prisma-resolve.js +15 -11
- package/dist/cli/prisma-schema.js +12 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +14 -2
- package/dist/cli/studio.js +246 -71
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +558 -85
- package/dist/client.js +1275 -400
- package/dist/connection-guard.d.ts +13 -0
- package/dist/connection-guard.js +27 -0
- package/dist/connection-url.d.ts +51 -16
- package/dist/connection-url.js +63 -34
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.js +144 -0
- package/dist/dialect.d.ts +449 -20
- package/dist/dialect.js +115 -43
- package/dist/engine-config.d.ts +7 -6
- package/dist/errors.d.ts +126 -6
- package/dist/errors.js +252 -12
- package/dist/generate.d.ts +57 -7
- package/dist/generate.js +635 -281
- package/dist/global-filters-config.d.ts +59 -0
- package/dist/global-filters-config.js +361 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/introspect.d.ts +31 -6
- package/dist/introspect.js +106 -50
- package/dist/log-codes.d.ts +12 -0
- package/dist/log-codes.js +74 -0
- package/dist/logger.d.ts +87 -0
- package/dist/logger.js +93 -0
- package/dist/mssql.d.ts +8 -5
- package/dist/mssql.js +354 -67
- package/dist/mysql.d.ts +3 -5
- package/dist/mysql.js +423 -73
- package/dist/nested-write.d.ts +3 -0
- package/dist/nested-write.js +177 -112
- package/dist/pipeline-submittable.d.ts +12 -0
- package/dist/pipeline-submittable.js +117 -44
- package/dist/pipeline.js +21 -26
- package/dist/powdb-introspect.d.ts +5 -2
- package/dist/powdb-introspect.js +21 -16
- package/dist/powdb-shared.d.ts +36 -5
- package/dist/powdb-shared.js +65 -8
- package/dist/powdb.d.ts +4 -0
- package/dist/powdb.js +183 -51
- package/dist/powql.d.ts +394 -51
- package/dist/powql.js +1836 -291
- package/dist/prisma-compat.d.ts +23 -15
- package/dist/prisma-compat.js +508 -373
- package/dist/query/aggregates.d.ts +17 -1
- package/dist/query/aggregates.js +263 -32
- package/dist/query/batched-loader.d.ts +25 -6
- package/dist/query/batched-loader.js +101 -21
- package/dist/query/builder.d.ts +325 -184
- package/dist/query/builder.js +1458 -495
- package/dist/query/column-ref.d.ts +61 -0
- package/dist/query/column-ref.js +66 -0
- package/dist/query/compound-unique.d.ts +21 -0
- package/dist/query/compound-unique.js +97 -19
- package/dist/query/deferred.d.ts +53 -16
- package/dist/query/expr.d.ts +217 -0
- package/dist/query/expr.js +848 -0
- package/dist/query/filters.d.ts +155 -53
- package/dist/query/filters.js +597 -72
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +4 -0
- package/dist/query/json-value.d.ts +43 -0
- package/dist/query/json-value.js +54 -0
- package/dist/query/normalize-args.d.ts +87 -0
- package/dist/query/normalize-args.js +517 -0
- package/dist/query/option-surface.d.ts +16 -4
- package/dist/query/option-surface.js +56 -13
- package/dist/query/relation-names.d.ts +14 -0
- package/dist/query/relation-names.js +77 -19
- package/dist/query/relations.d.ts +30 -3
- package/dist/query/relations.js +220 -67
- package/dist/query/row-lock.d.ts +49 -0
- package/dist/query/row-lock.js +131 -0
- package/dist/query/sql-comment.d.ts +161 -0
- package/dist/query/sql-comment.js +374 -0
- package/dist/query/stream-idle.d.ts +132 -0
- package/dist/query/stream-idle.js +203 -0
- package/dist/query/table-client.d.ts +101 -0
- package/dist/query/table-client.js +70 -0
- package/dist/query/types.d.ts +677 -298
- package/dist/query/utils.d.ts +105 -6
- package/dist/query/utils.js +414 -18
- package/dist/query/warn-registry.d.ts +18 -0
- package/dist/query/warn-registry.js +18 -0
- package/dist/query/where-compile.d.ts +138 -14
- package/dist/query/where-compile.js +508 -31
- package/dist/query/where.d.ts +159 -59
- package/dist/query/where.js +711 -248
- package/dist/query/writes.d.ts +112 -50
- package/dist/query/writes.js +1496 -202
- package/dist/query-events.d.ts +4 -2
- package/dist/query-events.js +3 -1
- package/dist/realtime.d.ts +7 -3
- package/dist/realtime.js +13 -2
- package/dist/schema-builder.d.ts +55 -4
- package/dist/schema-builder.js +381 -18
- package/dist/schema-metadata.d.ts +21 -1
- package/dist/schema-metadata.js +28 -7
- package/dist/schema-sql.d.ts +75 -5
- package/dist/schema-sql.js +494 -44
- package/dist/schema.d.ts +26 -5
- package/dist/schema.js +109 -13
- package/dist/sql-placeholders.d.ts +56 -0
- package/dist/sql-placeholders.js +200 -0
- package/dist/sqlite.d.ts +75 -20
- package/dist/sqlite.js +597 -99
- package/dist/supplied-pool-types.d.ts +48 -0
- package/dist/supplied-pool-types.js +247 -0
- package/dist/transaction-control.d.ts +51 -0
- package/dist/transaction-control.js +311 -0
- package/dist/transaction-state.d.ts +342 -0
- package/dist/transaction-state.js +696 -0
- package/dist/typed-sql.d.ts +37 -3
- package/dist/typed-sql.js +100 -8
- package/package.json +7 -4
- package/skills/turbine-orm/SKILL.md +56 -6
package/dist/sqlite.js
CHANGED
|
@@ -19,18 +19,24 @@
|
|
|
19
19
|
* - `RETURNING` + `ON CONFLICT … DO UPDATE` (SQLite ≥ 3.35) → create / upsert /
|
|
20
20
|
* update / delete return real rows in a single statement (`resultStrategy =
|
|
21
21
|
* 'returning'`, same as Postgres).
|
|
22
|
-
* - **Single-writer:** one connection / one
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* - **Single-writer:** one connection / one transaction at a time. On one
|
|
23
|
+
* client, statements from other callers wait (FIFO, up to `busyTimeoutMs`)
|
|
24
|
+
* while a transaction is open instead of running inside it; another
|
|
25
|
+
* process's writer gets `SQLITE_BUSY`. Both surface as the retryable
|
|
26
|
+
* `SerializationFailureError`. `journal_mode = WAL` is enabled for file
|
|
27
|
+
* databases to allow concurrent readers across connections.
|
|
25
28
|
* - **Unsupported (throw `UnsupportedFeatureError`):** pgvector distance ops,
|
|
26
29
|
* LISTEN/NOTIFY (`$listen` / `$notify`), RLS `sessionContext`. Advisory-lock
|
|
27
30
|
* migration locking is unavailable, SQLite is single-writer, so migrations
|
|
28
31
|
* serialize naturally.
|
|
29
|
-
* - **Type affinity caveats:** SQLite has no native `BOOLEAN` (0/1 integers)
|
|
30
|
-
* `DATE` (TEXT/INTEGER). Booleans bind as 1/0; `Date` values
|
|
31
|
-
* text;
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* - **Type affinity caveats:** SQLite has no native `BOOLEAN` (0/1 integers),
|
|
33
|
+
* `JSON` (text) or `DATE` (TEXT/INTEGER). Booleans bind as 1/0; `Date` values
|
|
34
|
+
* bind as ISO-8601 text; an integral JS number binds as INTEGER. A column the
|
|
35
|
+
* schema declares boolean or json/jsonb reads back as a boolean or a parsed
|
|
36
|
+
* value on every path; date/time columns are coerced back to `Date`.
|
|
37
|
+
* Integers wider than `Number.MAX_SAFE_INTEGER` come back as strings (the
|
|
38
|
+
* same safe-int policy Turbine uses for Postgres `int8`). `numeric` has no
|
|
39
|
+
* exact storage: it reads back as a JS number.
|
|
34
40
|
* - **Case-insensitive matching** uses `COLLATE NOCASE`, which is **ASCII-only**
|
|
35
41
|
* (no Unicode case folding).
|
|
36
42
|
*
|
|
@@ -68,7 +74,7 @@
|
|
|
68
74
|
*
|
|
69
75
|
* const db = turbineSqlite(':memory:', schemaDefToMetadata(schema));
|
|
70
76
|
* for (const stmt of schemaToSQL(schema, { dialect: sqliteDialect })) {
|
|
71
|
-
* await db.
|
|
77
|
+
* await db.rawUnsafe(stmt);
|
|
72
78
|
* }
|
|
73
79
|
*
|
|
74
80
|
* // `db.table(...)`, not `db.users`: the typed property accessors are emitted
|
|
@@ -77,13 +83,16 @@
|
|
|
77
83
|
* await db.disconnect();
|
|
78
84
|
* ```
|
|
79
85
|
*/
|
|
86
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
80
87
|
import { createRequire } from 'node:module';
|
|
81
|
-
import { TurbineClient } from './client.js';
|
|
82
|
-
import { postgresDialect, } from './dialect.js';
|
|
83
|
-
import { ConnectionError, UnsupportedFeatureError, ValidationError } from './errors.js';
|
|
84
|
-
import { applyTableFilters, deriveEngineRelations } from './introspect.js';
|
|
88
|
+
import { TurbineClient, } from './client.js';
|
|
89
|
+
import { onConflictAction, postgresDialect, } from './dialect.js';
|
|
90
|
+
import { ConnectionError, SerializationFailureError, UnsupportedFeatureError, ValidationError } from './errors.js';
|
|
91
|
+
import { applyTableFilters, assertDistinctColumnFields, deriveEngineRelations, nullRecord, printableCatalogName, } from './introspect.js';
|
|
92
|
+
import { withoutTrailingComment } from './query/sql-comment.js';
|
|
85
93
|
import { LRUCache } from './query/utils.js';
|
|
86
94
|
import { isDateType, snakeToCamel, } from './schema.js';
|
|
95
|
+
import { rewritePositionalPlaceholders } from './sql-placeholders.js';
|
|
87
96
|
let cachedDatabaseSync;
|
|
88
97
|
/**
|
|
89
98
|
* Lazily load `node:sqlite`'s `DatabaseSync` constructor.
|
|
@@ -121,7 +130,8 @@ function loadDatabaseSync() {
|
|
|
121
130
|
* Coerce an arbitrary JS value into something `node:sqlite` can bind.
|
|
122
131
|
*
|
|
123
132
|
* `node:sqlite` throws on `boolean`, `undefined`, `Date`, and plain objects, so
|
|
124
|
-
* we normalize: booleans → 1/0,
|
|
133
|
+
* we normalize: booleans → 1/0, a safe integer → INTEGER (see below),
|
|
134
|
+
* `undefined`/`null` → NULL, `Date` → ISO text,
|
|
125
135
|
* `Uint8Array`/`Buffer` → BLOB, and any remaining object/array → JSON text
|
|
126
136
|
* (matches how Turbine already pre-serializes JSON filter params).
|
|
127
137
|
*/
|
|
@@ -132,6 +142,12 @@ function toSqliteParam(value) {
|
|
|
132
142
|
case 'boolean':
|
|
133
143
|
return value ? 1 : 0;
|
|
134
144
|
case 'number':
|
|
145
|
+
// `node:sqlite` binds every JS number as REAL, so `views / 2` divided as
|
|
146
|
+
// floats (75 / 2 = 37.5, stored in an INTEGER column) and an integral
|
|
147
|
+
// number written to a TEXT column was stored as '3.0'. Binding a safe
|
|
148
|
+
// integer as an INTEGER gives the arithmetic and text PostgreSQL gives.
|
|
149
|
+
// -0 stays REAL: as an integer it would lose its sign.
|
|
150
|
+
return Number.isSafeInteger(value) && !Object.is(value, -0) ? BigInt(value) : value;
|
|
135
151
|
case 'bigint':
|
|
136
152
|
case 'string':
|
|
137
153
|
return value;
|
|
@@ -142,6 +158,44 @@ function toSqliteParam(value) {
|
|
|
142
158
|
return value; // also covers Buffer
|
|
143
159
|
return JSON.stringify(value);
|
|
144
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* The JSON text an `in` / `notIn` list binds as, element by element in the
|
|
163
|
+
* form {@link toSqliteParam} would bind it, so a list element compares exactly
|
|
164
|
+
* as the same value given to `equals` does. A bigint is written as a JSON
|
|
165
|
+
* integer (exact to 64 bits; `JSON.stringify` refuses one), an infinity as the
|
|
166
|
+
* overflowing literal SQLite's JSON parser reads as one, and a binary value as
|
|
167
|
+
* `{"b": "<hex>"}`, which {@link sqliteDialect.buildInClause} turns back into a
|
|
168
|
+
* BLOB. Any other object binds as its JSON text, as it would on its own.
|
|
169
|
+
*/
|
|
170
|
+
function sqliteInListJson(values) {
|
|
171
|
+
const parts = values.map((value) => {
|
|
172
|
+
if (value === undefined || value === null)
|
|
173
|
+
return 'null';
|
|
174
|
+
switch (typeof value) {
|
|
175
|
+
case 'boolean':
|
|
176
|
+
return value ? '1' : '0';
|
|
177
|
+
case 'number':
|
|
178
|
+
if (Number.isFinite(value))
|
|
179
|
+
return JSON.stringify(value);
|
|
180
|
+
if (Number.isNaN(value))
|
|
181
|
+
return 'null';
|
|
182
|
+
return value > 0 ? '9e999' : '-9e999';
|
|
183
|
+
case 'bigint':
|
|
184
|
+
return value.toString();
|
|
185
|
+
case 'string':
|
|
186
|
+
return JSON.stringify(value);
|
|
187
|
+
}
|
|
188
|
+
if (value instanceof Uint8Array) {
|
|
189
|
+
let hex = '';
|
|
190
|
+
for (const byte of value)
|
|
191
|
+
hex += byte.toString(16).padStart(2, '0');
|
|
192
|
+
return `{"b":"${hex}"}`;
|
|
193
|
+
}
|
|
194
|
+
const bound = toSqliteParam(value);
|
|
195
|
+
return JSON.stringify(bound);
|
|
196
|
+
});
|
|
197
|
+
return `[${parts.join(',')}]`;
|
|
198
|
+
}
|
|
145
199
|
/**
|
|
146
200
|
* Normalize a single column value read from SQLite. With `setReadBigInts(true)`
|
|
147
201
|
* every integer column comes back as a `bigint`; apply the same safe-integer
|
|
@@ -156,44 +210,142 @@ function normalizeValue(value) {
|
|
|
156
210
|
}
|
|
157
211
|
return value;
|
|
158
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* The decode a column of `type` needs on SQLite, or `undefined`. `type` is a
|
|
215
|
+
* declared SQLite type or a schema type (`bool`, `jsonb`). The ONE answer both
|
|
216
|
+
* read paths use: the driver shim for direct reads and {@link sqliteDialect}'s
|
|
217
|
+
* `jsonWireRule` for `with` rows, so the two cannot disagree about a column.
|
|
218
|
+
*/
|
|
219
|
+
function sqliteValueDecode(type) {
|
|
220
|
+
const t = type.toUpperCase();
|
|
221
|
+
if (t.includes('BOOL'))
|
|
222
|
+
return 'boolean';
|
|
223
|
+
if (t === 'JSON' || t === 'JSONB')
|
|
224
|
+
return 'json';
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
function declaredDecoders(schema) {
|
|
228
|
+
if (!schema)
|
|
229
|
+
return null;
|
|
230
|
+
let out = null;
|
|
231
|
+
for (const [table, meta] of Object.entries(schema.tables)) {
|
|
232
|
+
for (const column of meta.columns) {
|
|
233
|
+
const type = column.pgType ?? meta.pgTypes?.[column.name];
|
|
234
|
+
const decode = typeof type === 'string' ? sqliteValueDecode(type) : undefined;
|
|
235
|
+
if (decode === undefined)
|
|
236
|
+
continue;
|
|
237
|
+
if (out === null)
|
|
238
|
+
out = new Map();
|
|
239
|
+
out.set(`${table}\u0000${column.name}`, decode);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* A decimal integer or float as SQLite prints it (`printf('%!.17g')` / `CAST(... AS TEXT)`).
|
|
246
|
+
*
|
|
247
|
+
* Linear by construction: the fraction's digits can only follow a literal `.`,
|
|
248
|
+
* so a run of digits has exactly one way to match. The earlier `\d+\.?\d*`
|
|
249
|
+
* could split a dot-less run between its two quantifiers in every possible
|
|
250
|
+
* place, which made a long non-numeric cell take seconds to reject (quadratic:
|
|
251
|
+
* 40,000 digits and an `x` took 0.8 s, a million take 1.5 ms now).
|
|
252
|
+
*/
|
|
253
|
+
const SQLITE_NUMBER_TEXT = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
254
|
+
/**
|
|
255
|
+
* The text SQLite's INTEGER, REAL and NUMERIC affinities convert to a number,
|
|
256
|
+
* measured against SQLite 3.51: an optional sign, a decimal integer or float,
|
|
257
|
+
* an optional exponent, and ASCII whitespace around it. Anything else
|
|
258
|
+
* (`'abc'`, `''`, `'Infinity'`, `'0x10'`, `'1_000'`) is stored as TEXT in the
|
|
259
|
+
* numeric column. Linear for the same reason as {@link SQLITE_NUMBER_TEXT}.
|
|
260
|
+
*/
|
|
261
|
+
const SQLITE_NUMERIC_LITERAL = /^[ \t\n\v\f\r]*[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?[ \t\n\v\f\r]*$/i;
|
|
262
|
+
/**
|
|
263
|
+
* Declared column types whose SQLite affinity is numeric (INTEGER, REAL or
|
|
264
|
+
* NUMERIC), boolean included (stored 1/0). Matched whole, not by SQLite's own
|
|
265
|
+
* "contains INT" rule: the type a schema records for a column can be an enum's
|
|
266
|
+
* name (`point_kind`), which that rule reads as INTEGER while the column is
|
|
267
|
+
* TEXT. A type this misses is written as SQLite always wrote it, so a miss
|
|
268
|
+
* costs a refusal, never a wrong one.
|
|
269
|
+
*/
|
|
270
|
+
const SQLITE_NUMERIC_COLUMN_TYPE = /^(?:(?:tiny|small|medium|big)?int(?:eger)?|int[248]|(?:small|big)?serial|unsigned big int|real|float[48]?|double(?: precision)?|numeric|decimal|money|bool(?:ean)?)(?:\(\s*\d+\s*(?:,\s*\d+\s*)?\))?$/i;
|
|
271
|
+
/**
|
|
272
|
+
* Decode a REAL / NUMERIC cell carried as text through a `with` join back to
|
|
273
|
+
* the value a direct read returns: an integer by the safe-integer policy, a
|
|
274
|
+
* double as the number its 17 digits name, `Inf` / `-Inf` as infinities.
|
|
275
|
+
*/
|
|
276
|
+
function decodeRealText(value) {
|
|
277
|
+
if (typeof value !== 'string')
|
|
278
|
+
return value;
|
|
279
|
+
if (/^-?\d+$/.test(value)) {
|
|
280
|
+
const asNumber = Number(value);
|
|
281
|
+
return Number.isSafeInteger(asNumber) ? asNumber : value;
|
|
282
|
+
}
|
|
283
|
+
if (value === 'Inf')
|
|
284
|
+
return Number.POSITIVE_INFINITY;
|
|
285
|
+
if (value === '-Inf')
|
|
286
|
+
return Number.NEGATIVE_INFINITY;
|
|
287
|
+
return SQLITE_NUMBER_TEXT.test(value) ? Number(value) : value;
|
|
288
|
+
}
|
|
289
|
+
/** Read a JSON column's stored text as its value; text that is not JSON is returned as stored. */
|
|
290
|
+
function parseJsonCell(value) {
|
|
291
|
+
if (typeof value !== 'string')
|
|
292
|
+
return value;
|
|
293
|
+
try {
|
|
294
|
+
return JSON.parse(value);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return value;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
159
300
|
/**
|
|
160
301
|
* Convert a `node:sqlite` null-prototype row into a normalized plain object.
|
|
161
302
|
*
|
|
162
|
-
* `
|
|
163
|
-
*
|
|
164
|
-
* `true`/`false
|
|
165
|
-
*
|
|
303
|
+
* `decoders`, when present, names the result columns whose ORIGIN column is a
|
|
304
|
+
* boolean or a JSON column (see {@link resultColumnDecoders}): 1/0 becomes
|
|
305
|
+
* `true`/`false`, JSON text becomes its value. Anything else in such a column,
|
|
306
|
+
* and every column not listed, passes through untouched.
|
|
166
307
|
*/
|
|
167
|
-
function normalizeRow(row,
|
|
308
|
+
function normalizeRow(row, decoders) {
|
|
168
309
|
const out = {};
|
|
169
310
|
for (const key of Object.keys(row)) {
|
|
170
311
|
const value = normalizeValue(row[key]);
|
|
171
|
-
|
|
312
|
+
const decode = decoders?.get(key);
|
|
313
|
+
out[key] =
|
|
314
|
+
decode === undefined
|
|
315
|
+
? value
|
|
316
|
+
: decode === 'json'
|
|
317
|
+
? parseJsonCell(value)
|
|
318
|
+
: value === 1 || value === 0
|
|
319
|
+
? value === 1
|
|
320
|
+
: value;
|
|
172
321
|
}
|
|
173
322
|
return out;
|
|
174
323
|
}
|
|
175
324
|
/**
|
|
176
|
-
* The result columns of a prepared statement
|
|
177
|
-
*
|
|
325
|
+
* The result columns of a prepared statement that need decoding, or `null`
|
|
326
|
+
* when there are none (the overwhelmingly common case).
|
|
178
327
|
*
|
|
179
|
-
* WHY THIS EXISTS. SQLite has no boolean storage class: a
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* "this integer is a boolean", and it lives in the schema, not in the value.
|
|
328
|
+
* WHY THIS EXISTS. SQLite has no boolean or JSON storage class: a boolean is
|
|
329
|
+
* stored as 1/0 and a JSON document as text. Turbine still generates
|
|
330
|
+
* `ok: boolean` and a JSON value for those columns, and PostgreSQL returns
|
|
331
|
+
* exactly that, so a value the ORM itself wrote as `true` or `{ a: 1 }` came
|
|
332
|
+
* back as `1` or `'{"a":1}'`.
|
|
185
333
|
*
|
|
186
|
-
*
|
|
187
|
-
* because the driver knows which TABLE
|
|
188
|
-
* `StatementSync.columns()` reports
|
|
189
|
-
*
|
|
190
|
-
* expression, so a computed column
|
|
334
|
+
* Two sources name such a column, both read from the driver's per-column
|
|
335
|
+
* description, because only the driver knows which TABLE a result column came
|
|
336
|
+
* from. `StatementSync.columns()` reports the declared type
|
|
337
|
+
* (`sqlite3_column_decltype`) and the origin table and column, all of which are
|
|
338
|
+
* null for an expression, so a computed column is never decoded by accident.
|
|
339
|
+
* - A declared type naming a boolean (`BOOLEAN`), as before.
|
|
340
|
+
* - The client's schema: an origin column declared boolean or json/jsonb there.
|
|
341
|
+
* Turbine's own DDL stores those as INTEGER and TEXT, so the declared type
|
|
342
|
+
* alone missed every table Turbine created.
|
|
191
343
|
*
|
|
192
|
-
* Node gained `columns()` in 22.
|
|
193
|
-
* it is feature-detected: on the narrow band without it,
|
|
194
|
-
* back as
|
|
344
|
+
* Node gained `columns()` in 22.16 / 23.11 while this engine's floor is 22.5, so
|
|
345
|
+
* it is feature-detected: on the narrow band without it, these columns keep
|
|
346
|
+
* reading back as stored rather than the engine refusing to run.
|
|
195
347
|
*/
|
|
196
|
-
function
|
|
348
|
+
function resultColumnDecoders(stmt, declared) {
|
|
197
349
|
const columnsFn = stmt.columns;
|
|
198
350
|
if (typeof columnsFn !== 'function')
|
|
199
351
|
return null;
|
|
@@ -203,18 +355,24 @@ function booleanResultColumns(stmt) {
|
|
|
203
355
|
}
|
|
204
356
|
catch {
|
|
205
357
|
// A statement the driver cannot describe (some PRAGMA forms) is not worth
|
|
206
|
-
// failing a query over; skip the
|
|
358
|
+
// failing a query over; skip the decoding for it.
|
|
207
359
|
return null;
|
|
208
360
|
}
|
|
209
361
|
let out = null;
|
|
210
362
|
for (const c of described) {
|
|
211
|
-
if (typeof c.name !== 'string'
|
|
363
|
+
if (typeof c.name !== 'string')
|
|
212
364
|
continue;
|
|
213
|
-
|
|
365
|
+
let decode;
|
|
366
|
+
if (typeof c.type === 'string' && sqliteValueDecode(c.type) === 'boolean')
|
|
367
|
+
decode = 'boolean';
|
|
368
|
+
else if (declared !== null && typeof c.table === 'string' && typeof c.column === 'string') {
|
|
369
|
+
decode = declared.get(`${c.table}\u0000${c.column}`);
|
|
370
|
+
}
|
|
371
|
+
if (decode === undefined)
|
|
214
372
|
continue;
|
|
215
373
|
if (out === null)
|
|
216
|
-
out = new
|
|
217
|
-
out.
|
|
374
|
+
out = new Map();
|
|
375
|
+
out.set(c.name, decode);
|
|
218
376
|
}
|
|
219
377
|
return out;
|
|
220
378
|
}
|
|
@@ -228,11 +386,11 @@ let cachedSqliteNamespace;
|
|
|
228
386
|
/**
|
|
229
387
|
* Can the driver report a result column's DECLARED type?
|
|
230
388
|
*
|
|
231
|
-
* This is the same capability {@link
|
|
389
|
+
* This is the same capability {@link resultColumnDecoders} feature-detects, and
|
|
232
390
|
* the two have to be asked the SAME question or they split: that helper is what
|
|
233
391
|
* turns 1/0 into `true`/`false` on every direct read, and the boolean
|
|
234
392
|
* `jsonWireRule` is what does it on the join path. On the Node band with
|
|
235
|
-
* `node:sqlite` but without `StatementSync.columns()` (>= 22.5, < 22.
|
|
393
|
+
* `node:sqlite` but without `StatementSync.columns()` (>= 22.5, < 22.16 / 23.11)
|
|
236
394
|
* the helper returns `null` and every direct read keeps 1/0, so a join that
|
|
237
395
|
* still converted would produce exactly the strategy-dependent type flip the
|
|
238
396
|
* rule exists to prevent, on the one runtime where nothing else can catch it.
|
|
@@ -262,7 +420,12 @@ const ROW_RETURNING_HEAD = /^\s*(?:select|with|pragma|explain|values)\b/i;
|
|
|
262
420
|
* A `RETURNING` clause turns any write into a row-producing statement.
|
|
263
421
|
*/
|
|
264
422
|
function statementReturnsRows(sql) {
|
|
265
|
-
|
|
423
|
+
// A `sqlComment` (query/sql-comment.ts) sits after the statement and its
|
|
424
|
+
// values are data, so the RETURNING test reads the statement without it: a
|
|
425
|
+
// value such as `orders-returning` must not turn a write into a row read
|
|
426
|
+
// and its change count into 0.
|
|
427
|
+
const code = withoutTrailingComment(sql);
|
|
428
|
+
return ROW_RETURNING_HEAD.test(code) || /\breturning\b/i.test(code);
|
|
266
429
|
}
|
|
267
430
|
/**
|
|
268
431
|
* Augment a `node:sqlite` error with the Postgres-shaped fields that
|
|
@@ -334,18 +497,34 @@ function augmentSqliteError(err) {
|
|
|
334
497
|
target.code = '23514';
|
|
335
498
|
return err;
|
|
336
499
|
default:
|
|
337
|
-
// SQLITE_BUSY (5) / SQLITE_LOCKED (6)
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
500
|
+
// SQLITE_BUSY (5) / SQLITE_LOCKED (6) and their extended codes
|
|
501
|
+
// (SQLITE_BUSY_SNAPSHOT 517, SQLITE_LOCKED_SHAREDCACHE 262, ...). Built as
|
|
502
|
+
// the typed retryable error HERE rather than annotated, because the raw
|
|
503
|
+
// paths (`db.transaction(client => ...)`, `db.pool.query`) never pass
|
|
504
|
+
// through `wrapPgError` and would otherwise hand back the driver error.
|
|
505
|
+
if (primary === 5 || primary === 6)
|
|
506
|
+
return sqliteBusyError(e.errcode, message, err);
|
|
343
507
|
// SQLITE_ERROR (1): the statement itself is wrong. See the note above.
|
|
344
508
|
if (primary === 1)
|
|
345
509
|
return sqliteLogicError(message, err);
|
|
346
510
|
return err;
|
|
347
511
|
}
|
|
348
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* A `SQLITE_BUSY` / `SQLITE_LOCKED` failure as the retryable typed error, so
|
|
515
|
+
* `withRetry` / `$retry` and `err.isRetryable` handle it on every path.
|
|
516
|
+
* `busy_timeout` does not apply to a deferred transaction that read first and
|
|
517
|
+
* then tried to write (SQLITE_BUSY_SNAPSHOT): only retrying the WHOLE
|
|
518
|
+
* transaction can succeed, which is what the hint says.
|
|
519
|
+
*/
|
|
520
|
+
function sqliteBusyError(errcode, message, cause) {
|
|
521
|
+
const kind = (errcode & 0xff) === 5 ? 'SQLITE_BUSY' : 'SQLITE_LOCKED';
|
|
522
|
+
return new SerializationFailureError({
|
|
523
|
+
message: `SQLite database is busy (${kind}, code ${errcode}): ${message}. Another connection holds the lock it needs. ` +
|
|
524
|
+
'Retry the whole transaction (withRetry does), or raise busyTimeoutMs.',
|
|
525
|
+
cause,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
349
528
|
/**
|
|
350
529
|
* The `ValidationError` for a `SQLITE_ERROR`, with a hint for the two spellings
|
|
351
530
|
* that account for nearly all of them.
|
|
@@ -363,17 +542,17 @@ function augmentSqliteError(err) {
|
|
|
363
542
|
function sqliteLogicError(message, cause) {
|
|
364
543
|
const missingTable = /^no such table:\s*(\S+)/i.exec(message);
|
|
365
544
|
if (missingTable) {
|
|
366
|
-
return new ValidationError(`SQLite has no table "${missingTable[1]}": ${message}. ` +
|
|
545
|
+
return new ValidationError(`SQLite has no table "${printableCatalogName(missingTable[1])}": ${printableCatalogName(message)}. ` +
|
|
367
546
|
'Create it first (run your migrations, or execute the CREATE TABLE statements for this schema); ' +
|
|
368
547
|
'an in-memory database starts empty on every connection.', { cause });
|
|
369
548
|
}
|
|
370
549
|
const missingColumn = /^no such column:\s*(\S+)/i.exec(message);
|
|
371
550
|
if (missingColumn) {
|
|
372
|
-
return new ValidationError(`SQLite has no column "${missingColumn[1]}": ${message}. ` +
|
|
551
|
+
return new ValidationError(`SQLite has no column "${printableCatalogName(missingColumn[1])}": ${printableCatalogName(message)}. ` +
|
|
373
552
|
'Check the spelling against the table as it exists in this database, ' +
|
|
374
553
|
'and re-run `turbine generate` if the schema has changed.', { cause });
|
|
375
554
|
}
|
|
376
|
-
return new ValidationError(`SQLite rejected the statement: ${message}`, { cause });
|
|
555
|
+
return new ValidationError(`SQLite rejected the statement: ${printableCatalogName(message)}`, { cause });
|
|
377
556
|
}
|
|
378
557
|
function normalizeQueryArgs(arg, values) {
|
|
379
558
|
if (typeof arg === 'string')
|
|
@@ -403,8 +582,15 @@ function toNamedBinding(values) {
|
|
|
403
582
|
}
|
|
404
583
|
return named;
|
|
405
584
|
}
|
|
406
|
-
|
|
585
|
+
/** The refusal for a placeholder the shim cannot bind (see rewritePositionalPlaceholders). */
|
|
586
|
+
function refusePlaceholder(message) {
|
|
587
|
+
throw new ValidationError(message);
|
|
588
|
+
}
|
|
589
|
+
function runStatement(db, text, values, decoderCache, declared = null) {
|
|
407
590
|
const binding = toNamedBinding(values);
|
|
591
|
+
// `$n` is the spelling `rawUnsafe` documents for every engine; SQLite would
|
|
592
|
+
// read `$1` as a parameter NAMED "1", which the `{ p1, ... }` binding lacks.
|
|
593
|
+
const sql = rewritePositionalPlaceholders(text, 'sqlite', values.length, (i) => `:p${i}`, refusePlaceholder);
|
|
408
594
|
let stmt;
|
|
409
595
|
try {
|
|
410
596
|
stmt = db.prepare(sql);
|
|
@@ -415,15 +601,15 @@ function runStatement(db, sql, values, booleanCache) {
|
|
|
415
601
|
stmt.setReadBigInts(true);
|
|
416
602
|
try {
|
|
417
603
|
if (statementReturnsRows(sql)) {
|
|
418
|
-
// Describe each distinct statement ONCE. `null` (
|
|
604
|
+
// Describe each distinct statement ONCE. `null` (nothing to decode) is a
|
|
419
605
|
// cached answer too, so the ordinary query pays a single Map hit per call
|
|
420
606
|
// and nothing per row.
|
|
421
|
-
let
|
|
422
|
-
if (
|
|
423
|
-
|
|
424
|
-
|
|
607
|
+
let decoders = decoderCache?.get(sql);
|
|
608
|
+
if (decoders === undefined) {
|
|
609
|
+
decoders = resultColumnDecoders(stmt, declared);
|
|
610
|
+
decoderCache?.set(sql, decoders);
|
|
425
611
|
}
|
|
426
|
-
const rows = (binding ? stmt.all(binding) : stmt.all()).map((r) => normalizeRow(r,
|
|
612
|
+
const rows = (binding ? stmt.all(binding) : stmt.all()).map((r) => normalizeRow(r, decoders ?? undefined));
|
|
427
613
|
return { rows, rowCount: rows.length };
|
|
428
614
|
}
|
|
429
615
|
const info = binding ? stmt.run(binding) : stmt.run();
|
|
@@ -435,42 +621,96 @@ function runStatement(db, sql, values, booleanCache) {
|
|
|
435
621
|
throw augmentSqliteError(err);
|
|
436
622
|
}
|
|
437
623
|
}
|
|
624
|
+
/** Default wait, in ms, for a transaction another caller holds open on the handle. */
|
|
625
|
+
const DEFAULT_BUSY_TIMEOUT_MS = 5000;
|
|
626
|
+
const sqliteTxStorage = new AsyncLocalStorage();
|
|
627
|
+
/** Statement heads that leave the handle inside a transaction when they succeed. */
|
|
628
|
+
const TX_OPEN_HEAD = /^\s*(?:begin|savepoint)\b/i;
|
|
629
|
+
/** Statement heads that end the transaction (not `ROLLBACK TO`, which keeps it open). */
|
|
630
|
+
const TX_CLOSE_HEAD = /^\s*(?:commit|end|rollback(?!\s+(?:transaction\s+)?to\b))\b/i;
|
|
631
|
+
/**
|
|
632
|
+
* The one statement the pool can never run: something on the handle while the
|
|
633
|
+
* transaction that owns it is waiting for that very statement.
|
|
634
|
+
*/
|
|
635
|
+
function statementInsideTransactionError() {
|
|
636
|
+
return new UnsupportedFeatureError('A statement on the client while its own transaction is open', 'sqlite', 'This call was made through the client, not the `tx` client, from inside an open $transaction callback. ' +
|
|
637
|
+
'SQLite has one connection here, so the call would either join that transaction (and be rolled back with ' +
|
|
638
|
+
'it) or wait for it forever. Use the `tx` client the callback receives, or make the call after the ' +
|
|
639
|
+
'transaction completes.');
|
|
640
|
+
}
|
|
641
|
+
/** The error a caller gets when an open transaction holds the handle past `busyTimeoutMs`. */
|
|
642
|
+
function handleBusyError(timeoutMs) {
|
|
643
|
+
return new SerializationFailureError({
|
|
644
|
+
message: `SQLite database is busy: a transaction on this client held it for longer than busyTimeoutMs (${timeoutMs} ms). ` +
|
|
645
|
+
'Retry the operation (withRetry does), keep transactions short, or raise busyTimeoutMs.',
|
|
646
|
+
});
|
|
647
|
+
}
|
|
438
648
|
/**
|
|
439
649
|
* A `PgCompatPool` backed by a single `node:sqlite` `DatabaseSync` connection.
|
|
440
650
|
* SQLite is single-connection by nature (a `:memory:` database is per-handle),
|
|
441
|
-
* so `connect()` hands back a client over the **same** handle
|
|
442
|
-
*
|
|
443
|
-
*
|
|
651
|
+
* so `connect()` hands back a client over the **same** handle.
|
|
652
|
+
*
|
|
653
|
+
* One handle carries one transaction, so the pool decides who may use it. A
|
|
654
|
+
* caller whose statement opens a transaction OWNS the handle until that
|
|
655
|
+
* transaction ends; every other caller's statements, plain queries included,
|
|
656
|
+
* wait their turn in FIFO order (bounded by `busyTimeoutMs`) instead of running
|
|
657
|
+
* inside it. Without that, a write issued outside an open transaction joined
|
|
658
|
+
* it, reported success, and was rolled back with it, and reads saw its
|
|
659
|
+
* uncommitted rows. A checkout that does not open a transaction (a stream, a
|
|
660
|
+
* connectivity check) holds nothing, so other callers keep running beside it.
|
|
444
661
|
*/
|
|
445
662
|
export class SqlitePool {
|
|
446
663
|
/** The underlying `node:sqlite` handle, exposed as an escape hatch (seed/DDL). */
|
|
447
664
|
db;
|
|
448
665
|
closed = false;
|
|
449
666
|
/**
|
|
450
|
-
* `sql ->
|
|
667
|
+
* `sql -> result columns to decode` (or `null` for none). Bounded like
|
|
451
668
|
* the query builder's SQL-template cache, since a caller can generate
|
|
452
669
|
* unbounded distinct statements (raw SQL, an IN-list that inlines).
|
|
453
670
|
*/
|
|
454
|
-
|
|
455
|
-
|
|
671
|
+
decoderCache = new LRUCache(1000);
|
|
672
|
+
/** Schema columns whose stored form is not their declared JS type (see {@link declaredDecoders}). */
|
|
673
|
+
declared;
|
|
674
|
+
busyTimeoutMs;
|
|
675
|
+
/** Who may run statements on the handle right now; `null` when nobody holds it. */
|
|
676
|
+
owner = null;
|
|
677
|
+
waiters = [];
|
|
678
|
+
/** The owner for statements sent through `pool.query()` rather than a checkout. */
|
|
679
|
+
poolOwner = {};
|
|
680
|
+
/** Transaction state when the driver cannot report it (`isTransaction` arrived in Node 22.16). */
|
|
681
|
+
trackedTransaction = false;
|
|
682
|
+
constructor(db, options = {}) {
|
|
456
683
|
this.db = db;
|
|
684
|
+
const t = Number(options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS);
|
|
685
|
+
this.busyTimeoutMs = Number.isFinite(t) && t >= 0 ? t : DEFAULT_BUSY_TIMEOUT_MS;
|
|
686
|
+
this.declared = declaredDecoders(options.schema);
|
|
457
687
|
}
|
|
458
688
|
// biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape; runStatement returns plain objects.
|
|
459
689
|
async query(text, values) {
|
|
460
690
|
const { text: sql, params } = normalizeQueryArgs(text, values);
|
|
461
|
-
return
|
|
691
|
+
return this.runAs(this.poolOwner, sql, params);
|
|
462
692
|
}
|
|
463
693
|
async connect() {
|
|
464
|
-
|
|
465
|
-
|
|
694
|
+
if (this.closed)
|
|
695
|
+
throw new ConnectionError('This SQLite client has been disconnected.');
|
|
696
|
+
const owner = {};
|
|
697
|
+
let released = false;
|
|
466
698
|
return {
|
|
467
699
|
// biome-ignore lint/suspicious/noExplicitAny: see query() above.
|
|
468
700
|
query: async (text, values) => {
|
|
701
|
+
if (released) {
|
|
702
|
+
throw new ConnectionError('This SQLite connection was released (its transaction ended or timed out); ' +
|
|
703
|
+
'run the statement through the client or a new transaction.');
|
|
704
|
+
}
|
|
469
705
|
const { text: sql, params } = normalizeQueryArgs(text, values);
|
|
470
|
-
return
|
|
706
|
+
return this.runAs(owner, sql, params);
|
|
471
707
|
},
|
|
708
|
+
wrapTransactionCallback: (fn) => sqliteTxStorage.run({ pool: this, owner, parent: sqliteTxStorage.getStore() }, fn),
|
|
472
709
|
release: () => {
|
|
473
|
-
|
|
710
|
+
if (released)
|
|
711
|
+
return;
|
|
712
|
+
released = true;
|
|
713
|
+
this.releaseOwner(owner);
|
|
474
714
|
},
|
|
475
715
|
};
|
|
476
716
|
}
|
|
@@ -480,6 +720,103 @@ export class SqlitePool {
|
|
|
480
720
|
this.closed = true;
|
|
481
721
|
this.db.close();
|
|
482
722
|
}
|
|
723
|
+
/** Is the handle inside a transaction right now? */
|
|
724
|
+
inTransaction() {
|
|
725
|
+
const reported = this.db.isTransaction;
|
|
726
|
+
return typeof reported === 'boolean' ? reported : this.trackedTransaction;
|
|
727
|
+
}
|
|
728
|
+
/** Run one statement for `owner`, waiting first if another caller's transaction holds the handle. */
|
|
729
|
+
async runAs(owner, sql, params) {
|
|
730
|
+
if (this.closed)
|
|
731
|
+
throw new ConnectionError('This SQLite client has been disconnected.');
|
|
732
|
+
if (this.owner !== owner) {
|
|
733
|
+
if (this.owner !== null || this.waiters.length > 0) {
|
|
734
|
+
if (this.calledFromOwnersCallback())
|
|
735
|
+
throw statementInsideTransactionError();
|
|
736
|
+
await this.waitForHandle(owner);
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
this.owner = owner;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
try {
|
|
743
|
+
const result = runStatement(this.db, sql, params, this.decoderCache, this.declared);
|
|
744
|
+
if (TX_OPEN_HEAD.test(sql))
|
|
745
|
+
this.trackedTransaction = true;
|
|
746
|
+
else if (TX_CLOSE_HEAD.test(sql))
|
|
747
|
+
this.trackedTransaction = false;
|
|
748
|
+
return result;
|
|
749
|
+
}
|
|
750
|
+
finally {
|
|
751
|
+
// A statement that leaves no transaction open hands the handle on.
|
|
752
|
+
if (this.owner === owner && !this.inTransaction())
|
|
753
|
+
this.handOff();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
/** True when the current async context is the callback of the transaction that owns the handle. */
|
|
757
|
+
calledFromOwnersCallback() {
|
|
758
|
+
for (let m = sqliteTxStorage.getStore(); m; m = m.parent) {
|
|
759
|
+
if (m.pool === this && m.owner === this.owner)
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
waitForHandle(owner) {
|
|
765
|
+
return new Promise((resolve, reject) => {
|
|
766
|
+
let timer;
|
|
767
|
+
const waiter = {
|
|
768
|
+
owner,
|
|
769
|
+
settled: false,
|
|
770
|
+
grant: () => {
|
|
771
|
+
if (timer)
|
|
772
|
+
clearTimeout(timer);
|
|
773
|
+
resolve();
|
|
774
|
+
},
|
|
775
|
+
};
|
|
776
|
+
this.waiters.push(waiter);
|
|
777
|
+
timer = setTimeout(() => {
|
|
778
|
+
if (waiter.settled)
|
|
779
|
+
return;
|
|
780
|
+
waiter.settled = true;
|
|
781
|
+
const at = this.waiters.indexOf(waiter);
|
|
782
|
+
if (at >= 0)
|
|
783
|
+
this.waiters.splice(at, 1);
|
|
784
|
+
reject(handleBusyError(this.busyTimeoutMs));
|
|
785
|
+
}, this.busyTimeoutMs);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
/** Give the handle to the next waiter, which runs its statement as soon as it resumes. */
|
|
789
|
+
handOff() {
|
|
790
|
+
this.owner = null;
|
|
791
|
+
while (this.waiters.length > 0) {
|
|
792
|
+
const next = this.waiters.shift();
|
|
793
|
+
if (next.settled)
|
|
794
|
+
continue;
|
|
795
|
+
next.settled = true;
|
|
796
|
+
this.owner = next.owner;
|
|
797
|
+
next.grant();
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* A checkout ended. If it still holds an open transaction (a timed-out or
|
|
803
|
+
* abandoned callback), roll it back first, so the next caller does not
|
|
804
|
+
* start inside it.
|
|
805
|
+
*/
|
|
806
|
+
releaseOwner(owner) {
|
|
807
|
+
if (this.owner !== owner)
|
|
808
|
+
return;
|
|
809
|
+
if (this.inTransaction() && !this.closed) {
|
|
810
|
+
try {
|
|
811
|
+
this.db.exec('ROLLBACK');
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
// Nothing left to roll back.
|
|
815
|
+
}
|
|
816
|
+
this.trackedTransaction = false;
|
|
817
|
+
}
|
|
818
|
+
this.handOff();
|
|
819
|
+
}
|
|
483
820
|
}
|
|
484
821
|
// ---------------------------------------------------------------------------
|
|
485
822
|
// Type mapping (SQLite declared type → TypeScript)
|
|
@@ -547,6 +884,44 @@ function sqliteColumnAffinity(type) {
|
|
|
547
884
|
export const sqliteDialect = {
|
|
548
885
|
...postgresDialect,
|
|
549
886
|
name: 'sqlite',
|
|
887
|
+
capabilities: Object.freeze({
|
|
888
|
+
distinctOn: false,
|
|
889
|
+
positionalJson: false,
|
|
890
|
+
// NULLS FIRST / LAST since 3.30; NULL is the smallest value.
|
|
891
|
+
nullsOrderingClause: true,
|
|
892
|
+
nullsSortHigh: false,
|
|
893
|
+
insensitiveInList: false,
|
|
894
|
+
jsonDocumentEquality: 'text',
|
|
895
|
+
jsonExtractTyped: true,
|
|
896
|
+
jsonExactNumericCast: false,
|
|
897
|
+
numericTextComparison: false,
|
|
898
|
+
// Enforced only on a connection that turned `foreign_keys` on.
|
|
899
|
+
enforcesForeignKeys: false,
|
|
900
|
+
partitionLimitPushdown: false,
|
|
901
|
+
utcTimestampRewrite: false,
|
|
902
|
+
paramTypeCasts: false,
|
|
903
|
+
namedEnumTypes: false,
|
|
904
|
+
structuredValueColumns: 'scalars',
|
|
905
|
+
// The default SQLITE_MAX_VARIABLE_NUMBER.
|
|
906
|
+
maxBindParameters: 32766,
|
|
907
|
+
maxInsertRows: Number.POSITIVE_INFINITY,
|
|
908
|
+
upsertConflictTarget: 'explicit',
|
|
909
|
+
setClauseOrder: 'snapshot',
|
|
910
|
+
reselectLocksRows: false,
|
|
911
|
+
integerDivision: '/',
|
|
912
|
+
updateExpressions: true,
|
|
913
|
+
bulkUpsertAppliesRepeatedKeys: true,
|
|
914
|
+
readOnlyTransactions: false,
|
|
915
|
+
connectionParameters: false,
|
|
916
|
+
backendCancel: false,
|
|
917
|
+
idleTransactionTimeout: false,
|
|
918
|
+
abortsTransactionOnError: false,
|
|
919
|
+
connectionDriver: 'engine-driver',
|
|
920
|
+
preparesEveryStatement: false,
|
|
921
|
+
statementSyntax: 'sqlite',
|
|
922
|
+
inlineForeignKeys: true,
|
|
923
|
+
unboundedTextKeys: true,
|
|
924
|
+
}),
|
|
550
925
|
resultStrategy: 'returning', // SQLite ≥ 3.35 has RETURNING
|
|
551
926
|
supportsReturning: true,
|
|
552
927
|
supportsILike: false,
|
|
@@ -563,6 +938,13 @@ export const sqliteDialect = {
|
|
|
563
938
|
// No FROM-clause LATERAL: the opt-in lateral pick plan is Postgres-only.
|
|
564
939
|
supportsLateralJoin: false,
|
|
565
940
|
supportsPlanCacheMode: false,
|
|
941
|
+
// No row locks: a write transaction locks the whole database.
|
|
942
|
+
rowLockModes: [],
|
|
943
|
+
// percentile_cont / percentile_disc exist only in builds compiled with
|
|
944
|
+
// SQLITE_ENABLE_PERCENTILE, which node:sqlite cannot be assumed to have.
|
|
945
|
+
supportsPercentileAggregates: false,
|
|
946
|
+
// INSERT ... ON CONFLICT (cols) DO UPDATE ... RETURNING (3.24 + 3.35).
|
|
947
|
+
supportsBulkUpsert: true,
|
|
566
948
|
// SQLite explains a compiled query with `EXPLAIN QUERY PLAN` (four columns:
|
|
567
949
|
// id, parent, notused, detail), overriding the inherited Postgres `EXPLAIN`.
|
|
568
950
|
explainQuery: { prefix: 'EXPLAIN QUERY PLAN' },
|
|
@@ -626,7 +1008,7 @@ export const sqliteDialect = {
|
|
|
626
1008
|
}
|
|
627
1009
|
// A declared-boolean column is 1/0 in storage, and the driver shim turns
|
|
628
1010
|
// that back into true/false on every direct read (see
|
|
629
|
-
//
|
|
1011
|
+
// resultColumnDecoders). `json_object` sees the raw integer and has no
|
|
630
1012
|
// decltype to consult, so the join strategy alone would have kept handing
|
|
631
1013
|
// back 1/0 while the top-level read, the batched loader and the flatten
|
|
632
1014
|
// plan returned booleans, which is the same strategy-dependent type flip
|
|
@@ -638,7 +1020,8 @@ export const sqliteDialect = {
|
|
|
638
1020
|
// driverReportsDeclaredTypes). Without it the shim cannot name a boolean
|
|
639
1021
|
// result column, so 1/0 is the answer everywhere; converting here alone
|
|
640
1022
|
// would put the flip back, pointing the other way.
|
|
641
|
-
|
|
1023
|
+
const decode = sqliteValueDecode(columnType);
|
|
1024
|
+
if (decode === 'boolean' && driverReportsDeclaredTypes()) {
|
|
642
1025
|
return {
|
|
643
1026
|
// Carried as TEXT because that is how the decode half is reached at
|
|
644
1027
|
// all: the parser only runs `decode` on a string cell, so a rule that
|
|
@@ -651,6 +1034,29 @@ export const sqliteDialect = {
|
|
|
651
1034
|
decode: (value) => (value === '1' ? true : value === '0' ? false : value),
|
|
652
1035
|
};
|
|
653
1036
|
}
|
|
1037
|
+
// A json/jsonb column is JSON text in storage, which the shim parses on
|
|
1038
|
+
// every direct read (see resultColumnDecoders). `json_object` embeds the
|
|
1039
|
+
// same text as a JSON string, so the decode is the same parse. No cast: the
|
|
1040
|
+
// column reference already yields the text. Gated like the boolean rule,
|
|
1041
|
+
// since the direct-read half needs the same driver capability.
|
|
1042
|
+
if (decode === 'json' && driverReportsDeclaredTypes()) {
|
|
1043
|
+
return { sql: (ref) => ref, decode: parseJsonCell };
|
|
1044
|
+
}
|
|
1045
|
+
// `json_object` renders a REAL with 15 significant digits on the SQLite
|
|
1046
|
+
// that Node 22 and 24 bundle (3.47 to 3.51; 3.53 renders 17), so 0.1 + 0.2
|
|
1047
|
+
// came back 0.3 through a join and a NUMERIC past 15 digits was rounded
|
|
1048
|
+
// again on top of its REAL storage, while the top-level read and the
|
|
1049
|
+
// batched loader return the stored double. `printf('%!.17g')` prints every
|
|
1050
|
+
// double with enough digits to rebuild it, on every version; an INTEGER in
|
|
1051
|
+
// the same column is carried as its text and decoded by the safe-integer
|
|
1052
|
+
// policy above; text and NULL pass through as stored.
|
|
1053
|
+
if (/REAL|FLOA|DOUB|NUMERIC|DECIMAL|MONEY/.test(t)) {
|
|
1054
|
+
return {
|
|
1055
|
+
sql: (ref) => `CASE typeof(${ref}) WHEN 'real' THEN printf('%!.17g', ${ref}) ` +
|
|
1056
|
+
`WHEN 'integer' THEN CAST(${ref} AS TEXT) ELSE ${ref} END`,
|
|
1057
|
+
decode: decodeRealText,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
654
1060
|
// A BLOB cannot go into JSON at all: SQLite raises "JSON cannot hold BLOB
|
|
655
1061
|
// values" and the whole query fails with a raw SQL logic error, where the
|
|
656
1062
|
// batched loader returns the row fine. Carry hex and rebuild the bytes.
|
|
@@ -684,11 +1090,35 @@ export const sqliteDialect = {
|
|
|
684
1090
|
// No `= ANY(array)` in SQLite. `json_each` expands a single JSON-array param
|
|
685
1091
|
// into a row set, keeping ONE placeholder (so the SQL cache stays valid
|
|
686
1092
|
// regardless of list length) and handling the empty-list case correctly.
|
|
1093
|
+
// `x IN (SELECT ...)` compares like `x = ?`: the column's affinity and
|
|
1094
|
+
// collation apply to each element, as they do to a bound operand. JSON has no
|
|
1095
|
+
// bytes, so a binary element travels as `{"b": "<hex>"}` and is decoded back
|
|
1096
|
+
// to a BLOB here (see sqliteInListJson).
|
|
687
1097
|
buildInClause(expr, paramRef, negated) {
|
|
688
|
-
return `${expr} ${negated ? 'NOT IN' : 'IN'}
|
|
1098
|
+
return (`${expr} ${negated ? 'NOT IN' : 'IN'} ` +
|
|
1099
|
+
`(SELECT CASE WHEN type = 'object' THEN unhex(json_extract(value, '$.b')) ELSE value END ` +
|
|
1100
|
+
`FROM json_each(${paramRef}))`);
|
|
689
1101
|
},
|
|
690
1102
|
inClauseParam(values) {
|
|
691
|
-
return
|
|
1103
|
+
return sqliteInListJson(Array.isArray(values) ? values : []);
|
|
1104
|
+
},
|
|
1105
|
+
/**
|
|
1106
|
+
* A string written to a numeric or boolean column. SQLite's type affinity
|
|
1107
|
+
* keeps text it cannot convert as TEXT in that column, where PostgreSQL
|
|
1108
|
+
* refuses it at the server: `'abc'` in an INTEGER column, or `'true'` in a
|
|
1109
|
+
* boolean one (stored 1/0), which `{ flag: true }` then never matches. A
|
|
1110
|
+
* string that is a number (`'12.5'`, `'9007199254740993'`, `' 7 '`) is
|
|
1111
|
+
* accepted and converted by SQLite, as before.
|
|
1112
|
+
*/
|
|
1113
|
+
checkTextWrite(table, column, columnType, value) {
|
|
1114
|
+
if (!SQLITE_NUMERIC_COLUMN_TYPE.test(columnType.trim()))
|
|
1115
|
+
return undefined;
|
|
1116
|
+
if (SQLITE_NUMERIC_LITERAL.test(value))
|
|
1117
|
+
return undefined;
|
|
1118
|
+
const boolean = /BOOL/i.test(columnType);
|
|
1119
|
+
return (`The string written to "${column}" on "${table}", a column of type ${columnType}, is not a number, so SQLite ` +
|
|
1120
|
+
`would store it as text in that column; PostgreSQL refuses it. ` +
|
|
1121
|
+
(boolean ? 'Write true or false.' : 'Write a number, or a string holding one, such as "12.5".'));
|
|
692
1122
|
},
|
|
693
1123
|
buildReturningClause(selection = '*') {
|
|
694
1124
|
return ` RETURNING ${selection === '*' ? '*' : selection.join(', ')}`;
|
|
@@ -711,6 +1141,21 @@ export const sqliteDialect = {
|
|
|
711
1141
|
params: input.rowValues.flat(),
|
|
712
1142
|
};
|
|
713
1143
|
},
|
|
1144
|
+
buildBulkUpsertStatement(input) {
|
|
1145
|
+
// VALUES, not `INSERT ... SELECT`: after a SELECT, SQLite cannot tell a
|
|
1146
|
+
// trailing `ON CONFLICT` from the SELECT's own clauses. The rest is the
|
|
1147
|
+
// PostgreSQL grammar (3.24 upsert, 3.35 RETURNING), `excluded` included.
|
|
1148
|
+
let n = 0;
|
|
1149
|
+
const tuples = input.rowValues.map((row) => `(${row.map(() => this.paramPlaceholder(++n)).join(', ')})`).join(', ');
|
|
1150
|
+
const params = input.rowValues.flat();
|
|
1151
|
+
return {
|
|
1152
|
+
sql: `INSERT INTO ${input.table} (${input.columns.join(', ')}) VALUES ${tuples} ` +
|
|
1153
|
+
`ON CONFLICT (${input.conflictColumns.join(', ')}) ` +
|
|
1154
|
+
onConflictAction(input, params, { target: input.table, incoming: 'excluded' }) +
|
|
1155
|
+
this.buildReturningClause(input.returning),
|
|
1156
|
+
params,
|
|
1157
|
+
};
|
|
1158
|
+
},
|
|
714
1159
|
buildDefaultValuesInsertStatement(input) {
|
|
715
1160
|
// SQLite's `DEFAULT VALUES` inserts exactly one row and the engine accepts
|
|
716
1161
|
// neither `VALUES (DEFAULT, …)` nor MySQL's empty `VALUES ()` tuple, so
|
|
@@ -731,6 +1176,12 @@ export const sqliteDialect = {
|
|
|
731
1176
|
(input.updateWhere ? ` WHERE ${input.updateWhere}` : '') +
|
|
732
1177
|
this.buildReturningClause(input.returning));
|
|
733
1178
|
},
|
|
1179
|
+
// A SQLite transaction is always SERIALIZABLE, which satisfies every level
|
|
1180
|
+
// `$transaction({ isolationLevel })` can ask for, and the inherited
|
|
1181
|
+
// `BEGIN ISOLATION LEVEL ...` is a syntax error here.
|
|
1182
|
+
beginStatement() {
|
|
1183
|
+
return 'BEGIN';
|
|
1184
|
+
},
|
|
734
1185
|
buildInsensitiveLike(column, paramRef) {
|
|
735
1186
|
// COLLATE NOCASE is ASCII-only (no Unicode case folding), documented limit.
|
|
736
1187
|
return `${column} LIKE ${paramRef} COLLATE NOCASE`;
|
|
@@ -759,6 +1210,11 @@ export const sqliteDialect = {
|
|
|
759
1210
|
buildColumnType(input) {
|
|
760
1211
|
return sqliteColumnAffinity(input.type);
|
|
761
1212
|
},
|
|
1213
|
+
/** SQLite has no enum type: a TEXT column, and a CHECK that admits only the labels (compared exactly, BINARY collation). */
|
|
1214
|
+
buildEnumColumn(column, labels) {
|
|
1215
|
+
const literals = labels.map((label) => `'${label.replace(/'/g, "''")}'`);
|
|
1216
|
+
return { type: 'TEXT', check: `${column} IN (${literals.join(', ')})` };
|
|
1217
|
+
},
|
|
762
1218
|
buildColumnDefinition(input) {
|
|
763
1219
|
const isSerial = /serial/i.test(input.type);
|
|
764
1220
|
const parts = [input.name];
|
|
@@ -934,16 +1390,19 @@ export function introspectSqliteDatabase(db, options = {}) {
|
|
|
934
1390
|
// same logical schema, legacy-first naming, per-column disambiguation,
|
|
935
1391
|
// collision resolution against scalar column fields, and the conservative
|
|
936
1392
|
// pure-junction manyToMany auto-detection included.
|
|
937
|
-
|
|
1393
|
+
// One field per column, as the PostgreSQL introspector requires.
|
|
1394
|
+
for (const [tableName, cols] of columnsByTable)
|
|
1395
|
+
assertDistinctColumnFields(tableName, cols);
|
|
1396
|
+
const relationsByTable = deriveEngineRelations(tableNames, foreignKeys, pkByTable, columnsByTable, options.logger);
|
|
938
1397
|
// ----- Assemble TableMetadata -----
|
|
939
|
-
const tables =
|
|
1398
|
+
const tables = nullRecord();
|
|
940
1399
|
for (const tableName of tableNames) {
|
|
941
1400
|
const columns = columnsByTable.get(tableName) ?? [];
|
|
942
|
-
const columnMap =
|
|
943
|
-
const reverseColumnMap =
|
|
1401
|
+
const columnMap = nullRecord();
|
|
1402
|
+
const reverseColumnMap = nullRecord();
|
|
944
1403
|
const dateColumns = new Set();
|
|
945
|
-
const dialectTypes =
|
|
946
|
-
const pgTypes =
|
|
1404
|
+
const dialectTypes = nullRecord();
|
|
1405
|
+
const pgTypes = nullRecord();
|
|
947
1406
|
const allColumns = [];
|
|
948
1407
|
for (const col of columns) {
|
|
949
1408
|
columnMap[col.field] = col.name;
|
|
@@ -965,7 +1424,7 @@ export function introspectSqliteDatabase(db, options = {}) {
|
|
|
965
1424
|
allColumns,
|
|
966
1425
|
primaryKey: pkByTable.get(tableName) ?? [],
|
|
967
1426
|
uniqueColumns: uniqueByTable.get(tableName) ?? [],
|
|
968
|
-
relations: relationsByTable.get(tableName) ??
|
|
1427
|
+
relations: relationsByTable.get(tableName) ?? nullRecord(),
|
|
969
1428
|
indexes: indexesByTable.get(tableName) ?? [],
|
|
970
1429
|
};
|
|
971
1430
|
}
|
|
@@ -990,7 +1449,7 @@ export async function introspectSqlite(options) {
|
|
|
990
1449
|
throw new ConnectionError(`Failed to open SQLite database "${options.connectionString}": ${err.message}`);
|
|
991
1450
|
}
|
|
992
1451
|
try {
|
|
993
|
-
return introspectSqliteDatabase(db, { include: options.include, exclude: options.exclude });
|
|
1452
|
+
return introspectSqliteDatabase(db, { include: options.include, exclude: options.exclude, logger: options.logger });
|
|
994
1453
|
}
|
|
995
1454
|
finally {
|
|
996
1455
|
db.close();
|
|
@@ -1005,9 +1464,17 @@ function openSqliteDatabase(target, options) {
|
|
|
1005
1464
|
catch (err) {
|
|
1006
1465
|
throw new ConnectionError(`Failed to open SQLite database "${target}": ${err.message}`);
|
|
1007
1466
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1467
|
+
try {
|
|
1468
|
+
db.exec(`PRAGMA busy_timeout = ${Number(options.busyTimeoutMs ?? 5000)}`);
|
|
1469
|
+
if (options.foreignKeys !== false)
|
|
1470
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
1471
|
+
}
|
|
1472
|
+
catch (err) {
|
|
1473
|
+
// Opened here and about to be unreachable: close it rather than leave the
|
|
1474
|
+
// file handle to the garbage collector.
|
|
1475
|
+
db.close();
|
|
1476
|
+
throw err;
|
|
1477
|
+
}
|
|
1011
1478
|
if (target !== ':memory:' && options.wal !== false) {
|
|
1012
1479
|
try {
|
|
1013
1480
|
db.exec('PRAGMA journal_mode = WAL');
|
|
@@ -1038,16 +1505,47 @@ function openSqliteDatabase(target, options) {
|
|
|
1038
1505
|
* ```
|
|
1039
1506
|
*/
|
|
1040
1507
|
export function turbineSqlite(target, schema, options = {}) {
|
|
1041
|
-
const
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1508
|
+
const owns = typeof target === 'string';
|
|
1509
|
+
const db = owns ? openSqliteDatabase(target, options) : target;
|
|
1510
|
+
let pool;
|
|
1511
|
+
let client;
|
|
1512
|
+
try {
|
|
1513
|
+
pool = new SqlitePool(db, { busyTimeoutMs: options.busyTimeoutMs, schema });
|
|
1514
|
+
// Everything that is not a SQLite pragma is client config and rides through
|
|
1515
|
+
// untouched, so a new TurbineConfig option works here the day it lands. The
|
|
1516
|
+
// engine-owned keys come last, so no caller can unbind the dialect.
|
|
1517
|
+
const { wal: _wal, busyTimeoutMs: _busyTimeoutMs, foreignKeys: _foreignKeys, ...clientConfig } = options;
|
|
1518
|
+
client = new TurbineClient({
|
|
1519
|
+
...clientConfig,
|
|
1520
|
+
pool,
|
|
1521
|
+
dialect: sqliteDialect,
|
|
1522
|
+
preparedStatements: false,
|
|
1523
|
+
}, schema);
|
|
1524
|
+
}
|
|
1525
|
+
catch (err) {
|
|
1526
|
+
// A failed factory hands the caller nothing to close, so a handle it
|
|
1527
|
+
// OPENED is closed here (the client constructor refuses a bad config, a
|
|
1528
|
+
// static global filter naming no table or column for one). A handle the
|
|
1529
|
+
// caller passed in is the caller's and stays open.
|
|
1530
|
+
if (owns)
|
|
1531
|
+
db.close();
|
|
1532
|
+
throw err;
|
|
1533
|
+
}
|
|
1534
|
+
if (owns) {
|
|
1535
|
+
// Turbine opened this handle, so disconnect()/end() must close it, as the
|
|
1536
|
+
// other engine factories close a pool they built: the client sees
|
|
1537
|
+
// `TurbineConfig.pool` as external and would otherwise leave it open, still
|
|
1538
|
+
// answering queries after disconnect(). A handle the caller passed in stays
|
|
1539
|
+
// the caller's (disconnect() is a no-op for it).
|
|
1540
|
+
const baseDisconnect = client.disconnect.bind(client);
|
|
1541
|
+
const close = async (options) => {
|
|
1542
|
+
const result = options === undefined ? await baseDisconnect() : await baseDisconnect(options);
|
|
1543
|
+
await pool.end();
|
|
1544
|
+
return result ?? undefined;
|
|
1545
|
+
};
|
|
1546
|
+
const patch = client;
|
|
1547
|
+
patch.disconnect = close;
|
|
1548
|
+
patch.end = close;
|
|
1549
|
+
}
|
|
1550
|
+
return client;
|
|
1053
1551
|
}
|