@jarenjs/db 0.85.0 → 0.87.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/ARCHITECTURE.md +12 -1
- package/README.md +30 -15
- package/docs/HOSTS.md +131 -4
- package/docs/MODEL-FORMAT.md +6 -0
- package/docs/NATIVE-PLANS.md +5 -3
- package/docs/SQLITE-RELATIONAL.md +110 -4
- package/package.json +8 -4
- package/src/dialects/sqlite-relational.js +2 -1
- package/src/dialects/sqlite-schema.js +82 -32
- package/src/driver.js +3 -0
- package/src/drivers/node-process-endpoint.js +13 -0
- package/src/drivers/node-process.js +177 -0
- package/src/drivers/node-worker-endpoint.js +3 -103
- package/src/drivers/node-worker.js +6 -178
- package/src/drivers/node.js +1 -0
- package/src/drivers/sqlite-endpoint.js +113 -0
- package/src/drivers/worker-client.js +185 -0
- package/src/drivers/worker-protocol.js +15 -0
- package/src/errors.js +1 -0
- package/src/index.js +1 -1
- package/src/mutation.js +13 -16
- package/src/relational-api.js +1 -1
- package/src/table-migration.js +34 -1
- package/types/index.d.ts +3 -1
- package/types/node-process.d.ts +33 -0
- package/types/relational.d.ts +19 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -1186,7 +1186,9 @@ both hosts, including transaction failures.
|
|
|
1186
1186
|
|
|
1187
1187
|
`dialects/sqlite-relational.js` owns the explicit SQLite expression and statement
|
|
1188
1188
|
compiler; `dialects/sqlite-schema.js` reuses it for ordered tables, indexes and
|
|
1189
|
-
triggers
|
|
1189
|
+
triggers, sharing its column/reference renderer with native ADD COLUMN.
|
|
1190
|
+
`table-migration.js` owns reviewed source/settings guards for additive/object
|
|
1191
|
+
changes and source/target guards for native copying
|
|
1190
1192
|
and preservation checks, with catalog/pragma spellings in the SQLite dialect.
|
|
1191
1193
|
The supplied driver owns transactions and cursors. These programs are explicitly
|
|
1192
1194
|
SQLite-semantic and never enter the JSON residual evaluator. See
|
|
@@ -1196,3 +1198,12 @@ SQLite-semantic and never enter the JSON residual evaluator. See
|
|
|
1196
1198
|
does not import runtime owners. `/query`, `/model` and `/entity` expose those
|
|
1197
1199
|
mechanisms without the root store import. `compileEntityModel` performs one model
|
|
1198
1200
|
normalization for both entities and mapping; openStore reuses that result.
|
|
1201
|
+
`mutation.js` keeps bounded statement reuse keyed by emitted SQL; bindings,
|
|
1202
|
+
projections and limits are call-local, so cached plans do not retain old payloads.
|
|
1203
|
+
|
|
1204
|
+
`drivers/worker-client.js` and `drivers/sqlite-endpoint.js` own the shared bounded
|
|
1205
|
+
RPC contract. Thread and process entries supply their transports. The process
|
|
1206
|
+
driver retains owner credits until OS exit, fences lost generations, and combines
|
|
1207
|
+
observed native transaction state with pending statement metadata to distinguish
|
|
1208
|
+
rollback from an unknown commit. Supervision never serializes callbacks or replays
|
|
1209
|
+
transactions; Store ownership remains in the existing driver/Store gates.
|
package/README.md
CHANGED
|
@@ -888,7 +888,8 @@ oracle run on both. What differs is declared rather than discovered:
|
|
|
888
888
|
| `ALTER TABLE` | additive only | full, so a rebuild is never needed |
|
|
889
889
|
|
|
890
890
|
Neither backend has a **statement timeout** (`capabilities.statementTimeout`
|
|
891
|
-
is `false` on both) or **row estimates**.
|
|
891
|
+
is `false` on both) or **row estimates**. Portable replication uses SQLite
|
|
892
|
+
change capture; PostgreSQL capture and replication remain unqualified.
|
|
892
893
|
Cross-process concurrency on SQLite is its own story — WAL plus a busy
|
|
893
894
|
timeout, both set and visible on `store.capabilities`; on PostgreSQL it
|
|
894
895
|
is the server's, and a serialization failure or a deadlock arrives as
|
|
@@ -1211,16 +1212,18 @@ same report rows — with the one difference the PostgreSQL mapping
|
|
|
1211
1212
|
states: `numeric` carries both JSON number types, so an `integer`
|
|
1212
1213
|
member reads back as `number`.
|
|
1213
1214
|
|
|
1214
|
-
##
|
|
1215
|
+
## Replication and external-write boundaries
|
|
1215
1216
|
|
|
1216
|
-
The
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1217
|
+
The portable replication engine supplies replica identities, causal frontiers,
|
|
1218
|
+
bounded logical envelopes, durable replay receipts, conflict evidence and snapshot
|
|
1219
|
+
resets. Data and acknowledgements commit together; hosts supply transport and any
|
|
1220
|
+
conflict resolver. The default preserves both contenders and rejects the write.
|
|
1221
|
+
See [REPLICATION-FORMAT](docs/REPLICATION-FORMAT.md) for the current contract.
|
|
1222
|
+
|
|
1223
|
+
Capture observes participating Store writes on supported SQLite hosts. Arbitrary
|
|
1224
|
+
writes from another connection are not fine-grained capture events; `dataVersion()`
|
|
1225
|
+
provides coarse invalidation. Adopted triggers, unsupported layouts and PostgreSQL
|
|
1226
|
+
retain the explicit capture and replication qualification limits.
|
|
1224
1227
|
|
|
1225
1228
|
## Operating a store — configuration, maintenance, backup, cancellation, the queue
|
|
1226
1229
|
|
|
@@ -1311,10 +1314,11 @@ its side-effect-free status read are in
|
|
|
1311
1314
|
|
|
1312
1315
|
## What this is not — every non-claim in one place
|
|
1313
1316
|
|
|
1314
|
-
- **SQLite
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1317
|
+
- **SQLite and PostgreSQL have different capabilities.** Both backends ship;
|
|
1318
|
+
PostgreSQL does not provide the SQLite capture, replication, job queue or pragma
|
|
1319
|
+
maintenance capabilities. See the dialect table and normative host contracts.
|
|
1320
|
+
- **Replication transport is supplied by the host.** Portable logical replication
|
|
1321
|
+
ships; arbitrary external writes are not captured as local row events.
|
|
1318
1322
|
- **No statement timeout** on SQLite (the drivers expose no interrupt;
|
|
1319
1323
|
the capability slot is honestly `false`), no row estimates.
|
|
1320
1324
|
- **Not safe for mutually hostile tenants** without the profile's
|
|
@@ -1359,6 +1363,7 @@ Every subpath a consumer can import, derived from the manifest by
|
|
|
1359
1363
|
<!--fact:exports.db-->
|
|
1360
1364
|
| Import | Kind | Declarations |
|
|
1361
1365
|
|---|---|---|
|
|
1366
|
+
| `@jarenjs/db/node-process` | JavaScript | declared |
|
|
1362
1367
|
| `@jarenjs/db/search` | JavaScript | declared |
|
|
1363
1368
|
| `@jarenjs/db` | JavaScript | declared |
|
|
1364
1369
|
| `@jarenjs/db/node` | JavaScript | declared |
|
|
@@ -1436,9 +1441,19 @@ Native column reads and bounded mutation documents are specified in [NATIVE-PLAN
|
|
|
1436
1441
|
|
|
1437
1442
|
`@jarenjs/db/search` composes the resident ranker with bounded authoritative entity reads and optional atomic snapshot storage. See [persisted search](docs/SEARCH.md).
|
|
1438
1443
|
|
|
1439
|
-
Column-first table definitions, guarded same-connection rebuilds, exact matched writes,
|
|
1444
|
+
Column-first table definitions, native additive/object DDL, guarded same-connection rebuilds, exact matched writes,
|
|
1440
1445
|
partial conflicts, raw-text JSON queries and byte-valued operations use
|
|
1441
1446
|
[`@jarenjs/db/relational`](docs/SQLITE-RELATIONAL.md). Both host entries export
|
|
1442
1447
|
`snapshotDatabase(connection, newPath)` for a disk-backed committed-WAL copy.
|
|
1443
1448
|
Existing-connection consumers can import `/query`, `/model` and `/entity`;
|
|
1444
1449
|
`compileEntityModel` shares one normalization between queries and mapping.
|
|
1450
|
+
`sql.call('json_type', ...)` preserves native missing/null/type distinctions.
|
|
1451
|
+
`planSchemaChange`/`applySchemaChange` guard a reviewed ADD COLUMN, DROP INDEX,
|
|
1452
|
+
RENAME TABLE or DROP TABLE against source drift. Identity-changing policies stay
|
|
1453
|
+
explicit; ordinary rebuilds retain key and row preservation. Mutation statements
|
|
1454
|
+
are reused by SQL without retaining previous payload-bearing documents.
|
|
1455
|
+
|
|
1456
|
+
`@jarenjs/db/node-process` adds supervised native execution with finite owner
|
|
1457
|
+
admission, caller deadlines, generation fencing and separate process-exit and
|
|
1458
|
+
transaction-fate observations. See [execution hosts](docs/HOSTS.md#supervised-node-processes)
|
|
1459
|
+
for Store composition, receipt reconciliation and operating-system limits.
|
package/docs/HOSTS.md
CHANGED
|
@@ -77,6 +77,132 @@ run on the caller. Synchronous Store methods and live queries are unavailable on
|
|
|
77
77
|
these asynchronous connections. Bun can import both subpaths; opening a Node
|
|
78
78
|
SQLite worker there reports the named unavailable-binding failure (`JD0003`).
|
|
79
79
|
|
|
80
|
+
## Supervised Node processes
|
|
81
|
+
|
|
82
|
+
`nodeProcessDriver` from `@jarenjs/db/node-process` implements the same asynchronous
|
|
83
|
+
Driver and Connection contracts using one Node child process per connection. It
|
|
84
|
+
shares the worker protocol, cursor credits, transaction scopes and error handling
|
|
85
|
+
with the thread driver. SQLite and its native calls execute in the child; callbacks
|
|
86
|
+
and decoded query residuals still execute in the parent. No function is serialized.
|
|
87
|
+
Node.js 24 or newer is required. Bun can import the entry but `open` refuses with
|
|
88
|
+
`JD0003`. The native-call and process-lifecycle fixtures qualify Node 24.20.0 on
|
|
89
|
+
Linux; other supported operating systems require their own timing qualification.
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
import { nodeProcessDriver } from '@jarenjs/db/node-process';
|
|
93
|
+
|
|
94
|
+
const driver = nodeProcessDriver({ maxOwners: 2, timeoutMs: 250 });
|
|
95
|
+
const owner = await driver.open('application.sqlite', { timeout: 50, queueTimeout: 100 });
|
|
96
|
+
try {
|
|
97
|
+
const result = await owner.supervise(async (connection) => {
|
|
98
|
+
return connection.transaction(async (scope) => {
|
|
99
|
+
const statement = await scope.prepare('SELECT value FROM settings WHERE key = ?');
|
|
100
|
+
return statement.get(['theme']);
|
|
101
|
+
});
|
|
102
|
+
}, { signal: requestSignal, timeoutMs: 250 });
|
|
103
|
+
useResult(result);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error.code === 'JD2097' || error.code === 'JD2090') {
|
|
106
|
+
const observation = owner.settlement();
|
|
107
|
+
// This snapshot can still say quarantined / safeToReplace:false.
|
|
108
|
+
recordOwnerObservation(observation);
|
|
109
|
+
const exited = await owner.settled();
|
|
110
|
+
// Before repeating an uncertain write, inspect its durable receipt.
|
|
111
|
+
reconcileBeforeRetry(exited);
|
|
112
|
+
} else throw error;
|
|
113
|
+
} finally {
|
|
114
|
+
// Preserve the original operation outcome if closing a fenced generation fails.
|
|
115
|
+
await owner.close().catch(recordCleanupFailure);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`supervise(body,{signal,timeoutMs})` admits one supervised callback per connection.
|
|
120
|
+
An overlapping callback refuses with `JD2091`; an already-aborted call runs no
|
|
121
|
+
callback and leaves the owner healthy. A deadline or abort rejects with `JD2097`,
|
|
122
|
+
fences all pending calls and signals process termination. Later operations on the
|
|
123
|
+
old generation fail with `JD2090`. A normal callback failure propagates unchanged
|
|
124
|
+
and does not terminate a healthy owner. `cancel(reason)` explicitly fences the
|
|
125
|
+
whole connection and returns its cancellation error. It affects every call sharing
|
|
126
|
+
that owner, so independent request lifetimes should use separate owners.
|
|
127
|
+
|
|
128
|
+
This is response cancellation and owner termination, not a SQLite statement
|
|
129
|
+
interrupt. `capabilities.process` and `ownerTermination` are true, while
|
|
130
|
+
`capabilities.cancellation.midStatement` remains false. The supervision timer cannot
|
|
131
|
+
preempt synchronous parent JavaScript or bound operating-system scheduling. Keep
|
|
132
|
+
callbacks/residuals bounded and await all admitted work. Native fixtures use a
|
|
133
|
+
short deadline and independently enforce a one-second caller-response ceiling;
|
|
134
|
+
that tested ceiling is not a universal scheduling guarantee.
|
|
135
|
+
|
|
136
|
+
`settlement()` reports the generation, PID, health, transaction fate and
|
|
137
|
+
`safeToReplace`. `settled()` resolves only after the child's OS exit event. An
|
|
138
|
+
owner awaiting termination is quarantined and still consumes its credit. If the
|
|
139
|
+
OS cannot settle a process promptly, the response can finish while the owner
|
|
140
|
+
remains quarantined; neither capacity nor writer ownership is recycled. `restart()`
|
|
141
|
+
waits for confirmed exit and returns a new generation; old Store handles remain
|
|
142
|
+
invalid. No statement, transaction or callback is automatically replayed.
|
|
143
|
+
|
|
144
|
+
The transaction observation is conservative:
|
|
145
|
+
|
|
146
|
+
- A live, acknowledged native transaction is `active`.
|
|
147
|
+
- An acknowledged single-statement completion is `committed`, or `rolled-back`
|
|
148
|
+
after rollback. A multi-statement program with no open transaction remains
|
|
149
|
+
`unknown`: an acknowledgement alone cannot distinguish its internal boundaries.
|
|
150
|
+
- Confirmed process death rolls back an observed open transaction only when no
|
|
151
|
+
pending operation could have committed it.
|
|
152
|
+
- A lost commit reply, multi-statement program or uncertain autocommit mutation
|
|
153
|
+
is `unknown`. Native transaction-state support is observed, never inferred from
|
|
154
|
+
a close promise. Reopen, verify integrity and reconcile a durable receipt before
|
|
155
|
+
retrying a write with an unknown outcome.
|
|
156
|
+
|
|
157
|
+
Autocommit mutation cursors remain `unknown` until their final native frame is
|
|
158
|
+
acknowledged. Creating an iterator or receiving an intermediate `RETURNING` row
|
|
159
|
+
does not acknowledge completion; early cursor return still requires reconciliation.
|
|
160
|
+
|
|
161
|
+
Driver `metrics()` reports capacity, owners, healthy owners and quarantine. A
|
|
162
|
+
file path already owned by the same driver cannot be opened again until exit;
|
|
163
|
+
this reservation uses its resolved path spelling, not filesystem inode identity.
|
|
164
|
+
Different driver instances, aliases and external processes remain the host's
|
|
165
|
+
coordination responsibility. Use one supervisor per ownership domain and retain
|
|
166
|
+
finite SQLite busy and transaction-queue timeouts. The host's service manager must
|
|
167
|
+
also own its process group: an uncatchable parent-process death is not a promise
|
|
168
|
+
that JavaScript can reap every descendant. Child crashes and restart are covered
|
|
169
|
+
by the file/receipt fixtures; power loss and service-manager policy are separate.
|
|
170
|
+
|
|
171
|
+
The ordinary Store composition uses `openStore(model,{driver,path})`. To supervise
|
|
172
|
+
Store calls, retain the connection in a small driver wrapper's `open` method, then
|
|
173
|
+
call `owner.supervise(() => store.transaction(body), options)`. This uses the same
|
|
174
|
+
Store and transaction APIs. After owner loss, close the invalid Store, await owner
|
|
175
|
+
exit and explicitly reopen/reconcile. A failed-generation `close()` may reject;
|
|
176
|
+
its rejection never substitutes for the separate exit observation.
|
|
177
|
+
|
|
178
|
+
All options below are positive safe integers. Row/byte/statement/cursor limits
|
|
179
|
+
retain the thread worker meanings. `maxOwners` includes startup and quarantine;
|
|
180
|
+
`timeoutMs` is the default supervised response deadline. `maxRequestBytes` bounds
|
|
181
|
+
each outgoing request before IPC; parameters must be SQLite scalars or byte arrays.
|
|
182
|
+
The process close deadline
|
|
183
|
+
and startup deadline can reject while termination is still pending. The reserved
|
|
184
|
+
owner credit remains until exit. Direct calls without `supervise`
|
|
185
|
+
retain the ordinary row, queue and step cancellation boundaries.
|
|
186
|
+
|
|
187
|
+
<!--fact:db.execution-options-->
|
|
188
|
+
|
|
189
|
+
| Option | Thread worker | Process owner |
|
|
190
|
+
|---|---:|---:|
|
|
191
|
+
| `windowRows` | 64 | 64 |
|
|
192
|
+
| `windowBytes` | 1048576 | 1048576 |
|
|
193
|
+
| `maxPending` | 64 | 64 |
|
|
194
|
+
| `maxStatements` | 1024 | 1024 |
|
|
195
|
+
| `maxCursors` | 64 | 64 |
|
|
196
|
+
| `allMaxRows` | 100000 | 100000 |
|
|
197
|
+
| `allMaxBytes` | 16777216 | 16777216 |
|
|
198
|
+
| `closeTimeoutMs` | 5000 | 1000 |
|
|
199
|
+
| `startupTimeoutMs` | 10000 | 10000 |
|
|
200
|
+
| `maxOwners` | — | 4 |
|
|
201
|
+
| `timeoutMs` | — | 250 |
|
|
202
|
+
| `maxRequestBytes` | — | 1048576 |
|
|
203
|
+
|
|
204
|
+
<!--/fact-->
|
|
205
|
+
|
|
80
206
|
## WAL pool policy
|
|
81
207
|
|
|
82
208
|
A file pool opens exactly one writer and `readers` read-only workers, verifies WAL,
|
|
@@ -283,9 +409,10 @@ sync twin. Physical adoption on PostgreSQL refuses pending a separate mapping
|
|
|
283
409
|
and codec qualification. Unknown application-trigger effects do not qualify
|
|
284
410
|
capture or replication; those combinations refuse before an adoption claim.
|
|
285
411
|
|
|
286
|
-
Node backups use the built-in online snapshot. Bun uses `
|
|
287
|
-
under the store gate,
|
|
288
|
-
|
|
289
|
-
image
|
|
412
|
+
Node backups use the built-in online snapshot. Bun uses disk-backed `VACUUM INTO`
|
|
413
|
+
under the store gate, flushes a sibling temporary, then uses the shared atomic
|
|
414
|
+
publisher. Both include committed WAL. Bun allocates no whole-database JavaScript
|
|
415
|
+
image; native SQLite caches and temporary storage govern working memory, and
|
|
416
|
+
cancellation takes effect between phases. Process-kill tests
|
|
290
417
|
cover rebuild copy, table drop, commit and backup publication on both hosts;
|
|
291
418
|
these tests do not establish power-loss durability or native executable packaging.
|
package/docs/MODEL-FORMAT.md
CHANGED
|
@@ -1101,6 +1101,7 @@ error.
|
|
|
1101
1101
|
| `JD2094` | invalid or uncommitted durable snapshot; reopen the last committed version |
|
|
1102
1102
|
| `JD2095` | trusted SQL or synchronous callback authority refused |
|
|
1103
1103
|
| `JD2096` | persistence invariant rejected the mutation; constraint class |
|
|
1104
|
+
| `JD2097` | supervised native operation cancelled or past its response deadline; owner exit and transaction fate are reported separately |
|
|
1104
1105
|
| `JD0060` | a replication envelope or snapshot is invalid |
|
|
1105
1106
|
| `JD2100` | a replica sequence or causal dependency has a gap |
|
|
1106
1107
|
| `JD2101` | an envelope identity names different content or an unknown local origin |
|
|
@@ -2424,3 +2425,8 @@ physical entity retains adoption-only behavior. Public plans create complete
|
|
|
2424
2425
|
ordered declarations; an incomplete generated/default definition or view remains
|
|
2425
2426
|
adoption metadata. SQLite expressions preserve a separate, explicit semantic
|
|
2426
2427
|
contract for raw text, bytes, nulls, collations and floating totals.
|
|
2428
|
+
`planSchemaChange`/`applySchemaChange` expose explicit native ADD COLUMN, DROP
|
|
2429
|
+
INDEX, RENAME TABLE and DROP TABLE with source/settings guards. They do not infer
|
|
2430
|
+
identity remapping or row deletion; that policy stays in an explicitly reviewed
|
|
2431
|
+
transaction. Entity mutation statement reuse is keyed by emitted SQL, while
|
|
2432
|
+
bindings, output projections and resource limits belong to each execution.
|
package/docs/NATIVE-PLANS.md
CHANGED
|
@@ -65,9 +65,11 @@ row limits bound fetched results; byte limits bound each decoded result item.
|
|
|
65
65
|
|
|
66
66
|
## Mutations
|
|
67
67
|
|
|
68
|
-
The asynchronous entity set exposes `mutate(document)`. It compiles
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
The asynchronous entity set exposes `mutate(document)`. It compiles each closed
|
|
69
|
+
document into one parameterized SQLite data statement and reuses statements by
|
|
70
|
+
their complete SQL in a bounded cache. Bindings, projections and output limits
|
|
71
|
+
remain local to each call; earlier payload-bearing documents are not retained.
|
|
72
|
+
Execution uses the same guarded transaction as the entity writer. It supports adopted writable
|
|
71
73
|
column layouts. Hybrid entities, PostgreSQL physical layouts, arbitrary SQL,
|
|
72
74
|
store-enforced before/after invariants and unsupported expression shapes refuse
|
|
73
75
|
with `JD0038`. Database constraints and invariant triggers retain enforcement.
|
|
@@ -49,9 +49,16 @@ Selections support table and subquery sources, inner/left/cross joins, correlate
|
|
|
49
49
|
`sql.scalar`/`sql.exists`, CASE, IN/NOT IN, DISTINCT, grouped and distinct
|
|
50
50
|
aggregates, HAVING, UNION/UNION ALL and ordered windows. The declaration file
|
|
51
51
|
lists the closed operators and functions, including trim/coalesce, LIKE, JSON
|
|
52
|
-
extraction, casts and SQLite date functions. Neither arbitrary function names
|
|
52
|
+
extraction/type inspection, casts and SQLite date functions. Neither arbitrary function names
|
|
53
53
|
nor a raw-expression escape hatch is accepted.
|
|
54
54
|
|
|
55
|
+
`sql.call('json_type', [document, path])` distinguishes a missing path (SQL NULL)
|
|
56
|
+
from JSON null (text `'null'`) and reports SQLite's native scalar/container type
|
|
57
|
+
names. Omitting the path inspects the whole document. SQL-null input stays null;
|
|
58
|
+
malformed JSON raises SQLite's error. It composes with CASE and synchronous
|
|
59
|
+
cursors without decoding or rewriting the original column. See
|
|
60
|
+
[SQLite JSON type inspection](https://www.sqlite.org/json1.html#the_json_type_function).
|
|
61
|
+
|
|
55
62
|
## Exact writes and bytes
|
|
56
63
|
|
|
57
64
|
```js
|
|
@@ -114,7 +121,9 @@ applyTableMigration(connection, migration);
|
|
|
114
121
|
Columns retain declaration order and exact INTEGER/REAL/TEXT/BLOB/NUMERIC/ANY
|
|
115
122
|
types. Definitions support ordered primary keys, rowid or AUTOINCREMENT identity,
|
|
116
123
|
nullability, database defaults, generated columns, STRICT/WITHOUT ROWID, named
|
|
117
|
-
UNIQUE/CHECK/foreign-key constraints and delete/update actions.
|
|
124
|
+
UNIQUE/CHECK/foreign-key constraints and delete/update actions. A column can also
|
|
125
|
+
declare `references: { table, columns: [name], onDelete, onUpdate, deferred }`.
|
|
126
|
+
Index terms
|
|
118
127
|
support expressions, direction and collation, with an optional partial predicate.
|
|
119
128
|
Triggers support BEFORE/AFTER, INSERT/UPDATE/DELETE, UPDATE OF, OLD/NEW conditions,
|
|
120
129
|
mutation steps and RAISE. Schema expressions use the same structural emitter,
|
|
@@ -132,8 +141,9 @@ physical tables use the live-schema `planTableMigration` API.
|
|
|
132
141
|
## Guarded upgrades
|
|
133
142
|
|
|
134
143
|
Planning inspects the existing schema without modifying it. A differing existing
|
|
135
|
-
table requires `allowRebuild: true
|
|
136
|
-
|
|
144
|
+
table requires `allowRebuild: true` when using `planTableMigration`; use the narrow
|
|
145
|
+
schema operations below to append a column without rebuilding. Every removed
|
|
146
|
+
column needs `dropColumns`; removing an existing explicit
|
|
137
147
|
index/trigger requires `dropObjects`. Unmentioned indexes and triggers survive.
|
|
138
148
|
An optional `copy` maps writable non-key target columns to structural expressions;
|
|
139
149
|
new columns otherwise use their defaults. The plan retains the source schema and
|
|
@@ -157,6 +167,90 @@ refuses before DDL. Node/Bun regressions cover populated history and references,
|
|
|
157
167
|
failed copies, repeat reopening, nested rollback, and process death after DROP
|
|
158
168
|
with WAL recovery. These tests do not establish power-loss durability.
|
|
159
169
|
|
|
170
|
+
## Additive and object operations
|
|
171
|
+
|
|
172
|
+
`planSchemaChange(connection, operation)` returns a reviewable
|
|
173
|
+
`{ version, operation, sql, source, settings, checksum }`. It reads the main schema
|
|
174
|
+
and relevant connection settings without executing DDL. `applySchemaChange`
|
|
175
|
+
checks that source and settings again under an IMMEDIATE transaction before
|
|
176
|
+
executing the single statement. It returns `{ changed }`, the number of schema
|
|
177
|
+
operations that changed the catalog, rather than the number of affected rows.
|
|
178
|
+
Both methods require the same available synchronous SQLite ownership as rebuilds.
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
import { planSchemaChange, applySchemaChange, sql } from '@jarenjs/db/relational';
|
|
182
|
+
const addition = planSchemaChange(connection, {
|
|
183
|
+
op: 'addColumn', table: 'entries', column: {
|
|
184
|
+
name: 'revision', type: 'INTEGER', nullable: false, default: 1,
|
|
185
|
+
check: sql.binary('>=', sql.column('revision'), 1),
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
// Review addition.sql and addition.source before execution.
|
|
189
|
+
applySchemaChange(connection, addition);
|
|
190
|
+
applySchemaChange(connection, planSchemaChange(connection, {
|
|
191
|
+
op: 'dropIndex', name: 'obsolete_index', ifExists: true,
|
|
192
|
+
}));
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The closed operations are `addColumn` (`table`, `column`), `dropIndex` (`name`,
|
|
196
|
+
optional `ifExists`), `renameTable` (`table`, `to`) and `dropTable` (`table`,
|
|
197
|
+
optional `ifExists`). Identifiers address **main** explicitly; a temporary table
|
|
198
|
+
with the same spelling cannot redirect an operation. A dot inside a name is a
|
|
199
|
+
literal character. Attached database operations are not part of this surface.
|
|
200
|
+
The column definition and expression renderer are shared with `planTable`.
|
|
201
|
+
|
|
202
|
+
ADD COLUMN appends a declaration and preserves existing rowids, values, storage
|
|
203
|
+
classes, column order, indexes and triggers. It does not derive a complete table
|
|
204
|
+
definition, normalize unknown constraints or copy the table. Literal defaults
|
|
205
|
+
are supported; identity columns, STORED generated columns and expression defaults
|
|
206
|
+
refuse. Ordinary NOT NULL additions require a non-null default. REFERENCES
|
|
207
|
+
additions require a NULL default (or no default), and a single referenced column.
|
|
208
|
+
SQLite checks existing rows for a new CHECK or generated NOT NULL constraint;
|
|
209
|
+
those checks can scan the table even though the operation does not copy it.
|
|
210
|
+
See [SQLite ADD COLUMN restrictions](https://www.sqlite.org/lang_altertable.html#alter_table_add_column).
|
|
211
|
+
|
|
212
|
+
Drop operations fail on absence unless `ifExists: true` is supplied. A missing
|
|
213
|
+
object then returns `changed: 0`. DROP INDEX affects the named index, not its
|
|
214
|
+
table or triggers. DROP TABLE removes the table and its owned indexes/triggers;
|
|
215
|
+
SQLite's active foreign-key actions still apply. RENAME follows SQLite's current
|
|
216
|
+
dependency rewriting behavior and the reviewed `legacy_alter_table` setting.
|
|
217
|
+
See [DROP TABLE](https://www.sqlite.org/lang_droptable.html) and
|
|
218
|
+
[RENAME TABLE](https://www.sqlite.org/lang_altertable.html#alter_table_rename).
|
|
219
|
+
|
|
220
|
+
These are **single-source plans**, not durable migration receipts. Replan after
|
|
221
|
+
any schema change. Reapplying a successful ADD/RENAME plan refuses as stale;
|
|
222
|
+
it does not infer completion from a same-named object. An ordered migration must
|
|
223
|
+
check its trusted schema/version receipt before planning the next operation and
|
|
224
|
+
record completion in the same transaction. Unknown members or unsupported column
|
|
225
|
+
declarations refuse with `JD0005`; modified/stale plans refuse with `JD0021`.
|
|
226
|
+
Native object, data-constraint and dependency errors are left to SQLite. A
|
|
227
|
+
checksum detects accidental plan edits; it does not authorize untrusted plans.
|
|
228
|
+
|
|
229
|
+
## Explicit identity-changing upgrades
|
|
230
|
+
|
|
231
|
+
The ordinary rebuild planner continues to refuse key reassignment or row loss.
|
|
232
|
+
A reviewed upgrade can use the primitive operations under
|
|
233
|
+
`withForeignKeysSuspended`, with its own explicit data policy:
|
|
234
|
+
|
|
235
|
+
1. Check the durable migration receipt or the exact supported legacy schema.
|
|
236
|
+
2. Create a distinct replacement table with `planTable`.
|
|
237
|
+
3. Use relational insert-select with a scoped correlated selection, deterministic
|
|
238
|
+
identity choice and explicit exclusion predicate. Assert selected, inserted
|
|
239
|
+
and excluded counts, and any business-specific preservation requirements.
|
|
240
|
+
4. Plan/apply `dropTable` for the original and then `renameTable` for the
|
|
241
|
+
replacement. Create the required indexes/triggers and validate dependents.
|
|
242
|
+
5. Record completion inside the same transaction so another opening does no work.
|
|
243
|
+
|
|
244
|
+
Create the replacement before dropping the original; renaming the original first
|
|
245
|
+
can redirect dependent references. Plan each primitive inside the FK scope, after
|
|
246
|
+
the preceding schema operation. The helper checks references before commit and
|
|
247
|
+
restores connection settings. The caller must review incoming key references,
|
|
248
|
+
views, triggers, immutable neighbors and each row disposition. Conflicts fail and
|
|
249
|
+
roll back; there is no inferred permission to discard rows. The installed native
|
|
250
|
+
[qualification fixture](../../../test/db/fixtures/schema-upgrade.mjs) demonstrates
|
|
251
|
+
scoped minimum-ID selection, explicit exclusions, rollback, repeated reopening
|
|
252
|
+
and recovery after actual process death at DROP.
|
|
253
|
+
|
|
160
254
|
## Read-only inspection and disk snapshots
|
|
161
255
|
|
|
162
256
|
`readSchema` from `@jarenjs/db/model` inventories tables without adopting them,
|
|
@@ -188,3 +282,15 @@ calls. No global strong model cache is introduced. Keep connection/query caches
|
|
|
188
282
|
bounded and reuse compiled metadata. Smaller import graphs alone do not prove
|
|
189
283
|
the complete application meets its RSS budget; measure application memory with
|
|
190
284
|
representative workloads.
|
|
285
|
+
|
|
286
|
+
Entity mutation engines retain prepared statements by their complete emitted SQL
|
|
287
|
+
in the bounded core LRU cache. Bound values, output projections and row/byte limits
|
|
288
|
+
belong to each execution; changing a payload does not retain another document and
|
|
289
|
+
another copy of the same statement. Distinct SQL stays isolated. This reduces
|
|
290
|
+
retained payload memory, at the cost of rebinding/compiling an identical mutation
|
|
291
|
+
document on each call. The synthetic retention probe at
|
|
292
|
+
`test/db/fixtures/mutation-memory.mjs` reports both varying-payload and identical
|
|
293
|
+
mutation timings, heap and RSS on Node (`--expose-gc`) and Bun. Such samples do not
|
|
294
|
+
replace a complete application's resource gate. Metadata remains caller-owned;
|
|
295
|
+
keep one compiled mapping per used model, one query state per connection, and
|
|
296
|
+
release facade/cache references on close. Driver cursors remain ephemeral.
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/db",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.87.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./types/index.d.ts",
|
|
8
8
|
"sideEffects": false,
|
|
9
9
|
"exports": {
|
|
10
|
+
"./node-process": {
|
|
11
|
+
"types": "./types/node-process.d.ts",
|
|
12
|
+
"default": "./src/drivers/node-process.js"
|
|
13
|
+
},
|
|
10
14
|
"./search": {
|
|
11
15
|
"types": "./types/search.d.ts",
|
|
12
16
|
"default": "./src/search.js"
|
|
@@ -104,9 +108,9 @@
|
|
|
104
108
|
"prepack": "npm run build:types"
|
|
105
109
|
},
|
|
106
110
|
"dependencies": {
|
|
107
|
-
"@jarenjs/core": "^0.
|
|
108
|
-
"@jarenjs/json": "^0.
|
|
109
|
-
"@jarenjs/validate": "^0.
|
|
111
|
+
"@jarenjs/core": "^0.87.0",
|
|
112
|
+
"@jarenjs/json": "^0.87.0",
|
|
113
|
+
"@jarenjs/validate": "^0.87.0"
|
|
110
114
|
},
|
|
111
115
|
"bin": {
|
|
112
116
|
"jaren-db": "./src/cli.js"
|
|
@@ -22,7 +22,7 @@ export function relationalIdentifier(name) {
|
|
|
22
22
|
}
|
|
23
23
|
const q = relationalIdentifier;
|
|
24
24
|
const binary = new Set(['=', '<>', '<', '<=', '>', '>=', 'IS', 'IS NOT', '+', '-', '*', '/', '%', '||', 'AND', 'OR', 'LIKE', 'NOT LIKE', 'GLOB']);
|
|
25
|
-
const functions = new Set(['coalesce', 'nullif', 'trim', 'ltrim', 'rtrim', 'lower', 'upper', 'length', 'abs', 'round', 'typeof', 'json_extract', 'json_valid', 'count', 'sum', 'total', 'avg', 'min', 'max', 'date', 'time', 'datetime', 'julianday', 'unixepoch', 'strftime']);
|
|
25
|
+
const functions = new Set(['coalesce', 'nullif', 'trim', 'ltrim', 'rtrim', 'lower', 'upper', 'length', 'abs', 'round', 'typeof', 'json_extract', 'json_valid', 'json_type', 'count', 'sum', 'total', 'avg', 'min', 'max', 'date', 'time', 'datetime', 'julianday', 'unixepoch', 'strftime']);
|
|
26
26
|
const types = new Set(['INTEGER', 'REAL', 'TEXT', 'BLOB', 'NUMERIC']);
|
|
27
27
|
const collations = new Set(['BINARY', 'NOCASE', 'RTRIM']);
|
|
28
28
|
const node = (kind, spec) => ({ $sql: kind, ...spec });
|
|
@@ -104,6 +104,7 @@ export function relationalEmitter(options = {}) {
|
|
|
104
104
|
if (value.distinct !== undefined && typeof value.distinct !== 'boolean') fail('DISTINCT must be boolean');
|
|
105
105
|
if (value.distinct && value.args.length !== 1) fail('DISTINCT functions require one argument');
|
|
106
106
|
if (!value.args.length && value.name !== 'count') fail('SQL function requires arguments');
|
|
107
|
+
if (value.name === 'json_type' && value.args.length > 2) fail('json_type requires one or two arguments');
|
|
107
108
|
return `${value.name.toUpperCase()}(${value.distinct ? 'DISTINCT ' : ''}${value.args.length ? value.args.map(next).join(', ') : '*'})`;
|
|
108
109
|
}
|
|
109
110
|
case 'cast':
|
|
@@ -34,37 +34,13 @@ export function planTable(definition) {
|
|
|
34
34
|
for (const key of ['constraints', 'indexes', 'triggers']) if (definition[key] !== undefined && !Array.isArray(definition[key])) fail(`${key} must be a list`);
|
|
35
35
|
const emitter = relationalEmitter({ inline: true });
|
|
36
36
|
const names = new Set();
|
|
37
|
-
let inlineKey = false;
|
|
38
37
|
const columns = definition.columns.map((column) => {
|
|
39
|
-
|
|
40
|
-
const name = q(column.name);
|
|
38
|
+
const text = columnSql(column, definition, emitter);
|
|
41
39
|
if (names.has(column.name.toLowerCase())) fail('physical column names must be distinct');
|
|
42
40
|
names.add(column.name.toLowerCase());
|
|
43
|
-
|
|
44
|
-
if (definition.strict && column.type === 'NUMERIC') fail('STRICT tables do not support NUMERIC');
|
|
45
|
-
if (column.nullable !== undefined && typeof column.nullable !== 'boolean') fail('nullable must be boolean');
|
|
46
|
-
if (column.stored !== undefined && (typeof column.stored !== 'boolean' || column.generated === undefined)) fail('stored requires a generated expression');
|
|
47
|
-
let out = `${name} ${column.type}`;
|
|
48
|
-
if (column.identity !== undefined) {
|
|
49
|
-
if (!['rowid', 'autoincrement'].includes(column.identity) || column.type !== 'INTEGER'
|
|
50
|
-
|| definition.withoutRowid || definition.primaryKey?.length !== 1
|
|
51
|
-
|| definition.primaryKey[0] !== column.name || column.generated !== undefined) fail('identity requires a single INTEGER rowid primary key');
|
|
52
|
-
inlineKey = true;
|
|
53
|
-
out += ` PRIMARY KEY${column.identity === 'autoincrement' ? ' AUTOINCREMENT' : ''}`;
|
|
54
|
-
}
|
|
55
|
-
if (column.nullable === false) out += ' NOT NULL';
|
|
56
|
-
if (column.collation !== undefined) {
|
|
57
|
-
if (!['BINARY', 'NOCASE', 'RTRIM'].includes(column.collation)) fail('unsupported column collation');
|
|
58
|
-
out += ` COLLATE ${column.collation}`;
|
|
59
|
-
}
|
|
60
|
-
if (Object.hasOwn(column, 'default')) out += ` DEFAULT (${emitter.expr(column.default)})`;
|
|
61
|
-
if (column.check !== undefined) out += ` CHECK (${emitter.expr(column.check)})`;
|
|
62
|
-
if (column.generated !== undefined) {
|
|
63
|
-
if (Object.hasOwn(column, 'default')) fail('a generated column cannot have a default');
|
|
64
|
-
out += ` GENERATED ALWAYS AS (${emitter.expr(column.generated)}) ${column.stored ? 'STORED' : 'VIRTUAL'}`;
|
|
65
|
-
}
|
|
66
|
-
return out;
|
|
41
|
+
return text;
|
|
67
42
|
});
|
|
43
|
+
const inlineKey = definition.columns.some((column) => column.identity !== undefined);
|
|
68
44
|
const members = (values) => {
|
|
69
45
|
const text = list(values, 'constraint columns');
|
|
70
46
|
if (values.some((name) => !names.has(name.toLowerCase()))) fail('constraint names an undeclared column');
|
|
@@ -79,11 +55,12 @@ export function planTable(definition) {
|
|
|
79
55
|
else if (constraint.kind === 'check') columns.push(`${prefix}CHECK (${emitter.expr(constraint.expression)})`);
|
|
80
56
|
else if (constraint.kind === 'foreignKey') {
|
|
81
57
|
if (constraint.columns?.length !== constraint.references?.length) fail('foreign-key columns must have equal arity');
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
58
|
+
columns.push(`${prefix}FOREIGN KEY (${members(constraint.columns)}) ${referenceSql({
|
|
59
|
+
table: constraint.table, columns: constraint.references,
|
|
60
|
+
...(constraint.onDelete === undefined ? {} : { onDelete: constraint.onDelete }),
|
|
61
|
+
...(constraint.onUpdate === undefined ? {} : { onUpdate: constraint.onUpdate }),
|
|
62
|
+
...(constraint.deferred === undefined ? {} : { deferred: constraint.deferred }),
|
|
63
|
+
})}`);
|
|
87
64
|
}
|
|
88
65
|
else fail('constraint kind is unique, check or foreignKey');
|
|
89
66
|
const keys = constraint.kind === 'unique' ? ['kind', 'name', 'columns']
|
|
@@ -140,3 +117,76 @@ export function planTable(definition) {
|
|
|
140
117
|
indexes: (definition.indexes ?? []).map((i) => ({ name: i.name, unique: i.unique === true, terms: i.terms })),
|
|
141
118
|
} };
|
|
142
119
|
}
|
|
120
|
+
|
|
121
|
+
/** One REFERENCES clause shared by table constraints and column declarations. */
|
|
122
|
+
function referenceSql(reference) {
|
|
123
|
+
check(reference, ['table', 'columns', 'onDelete', 'onUpdate', 'deferred'], 'reference');
|
|
124
|
+
if (reference.deferred !== undefined && typeof reference.deferred !== 'boolean') fail('deferred must be boolean');
|
|
125
|
+
return `REFERENCES ${q(reference.table)} (${list(reference.columns, 'references')})`
|
|
126
|
+
+ (reference.onDelete === undefined ? '' : ` ON DELETE ${action(reference.onDelete)}`)
|
|
127
|
+
+ (reference.onUpdate === undefined ? '' : ` ON UPDATE ${action(reference.onUpdate)}`)
|
|
128
|
+
+ (reference.deferred ? ' DEFERRABLE INITIALLY DEFERRED' : '');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Render one typed column without reconstructing any surrounding schema. */
|
|
132
|
+
function columnSql(column, context, emitter) {
|
|
133
|
+
check(column, ['name', 'type', 'nullable', 'default', 'collation', 'identity', 'check', 'generated', 'stored', 'references'], 'column definition');
|
|
134
|
+
const name = q(column.name);
|
|
135
|
+
if (!['INTEGER', 'REAL', 'TEXT', 'BLOB', 'NUMERIC', 'ANY'].includes(column.type)) fail('unsupported SQLite column type');
|
|
136
|
+
if (context.strict && column.type === 'NUMERIC') fail('STRICT tables do not support NUMERIC');
|
|
137
|
+
if (column.nullable !== undefined && typeof column.nullable !== 'boolean') fail('nullable must be boolean');
|
|
138
|
+
if (column.stored !== undefined && (typeof column.stored !== 'boolean' || column.generated === undefined)) fail('stored requires a generated expression');
|
|
139
|
+
const hasDefault = Object.hasOwn(column, 'default');
|
|
140
|
+
if (context.additive) {
|
|
141
|
+
if (column.identity !== undefined || column.stored === true) fail('ADD COLUMN cannot add an identity or STORED column');
|
|
142
|
+
const value = column.default?.$sql === 'value' ? column.default.value : column.default;
|
|
143
|
+
if (hasDefault && !(value === null || typeof value === 'string' || typeof value === 'bigint'
|
|
144
|
+
|| (typeof value === 'number' && Number.isFinite(value)))) fail('ADD COLUMN requires a literal default');
|
|
145
|
+
if (column.generated === undefined && column.nullable === false && (!hasDefault || value === null)) fail('ADD COLUMN NOT NULL requires a non-null default');
|
|
146
|
+
if (column.references !== undefined && hasDefault && value !== null) fail('ADD COLUMN REFERENCES requires a NULL default');
|
|
147
|
+
}
|
|
148
|
+
let out = `${name} ${column.type}`;
|
|
149
|
+
if (column.identity !== undefined) {
|
|
150
|
+
if (!['rowid', 'autoincrement'].includes(column.identity) || column.type !== 'INTEGER'
|
|
151
|
+
|| context.withoutRowid || context.primaryKey?.length !== 1
|
|
152
|
+
|| context.primaryKey[0] !== column.name || column.generated !== undefined) fail('identity requires a single INTEGER rowid primary key');
|
|
153
|
+
out += ` PRIMARY KEY${column.identity === 'autoincrement' ? ' AUTOINCREMENT' : ''}`;
|
|
154
|
+
}
|
|
155
|
+
if (column.nullable === false) out += ' NOT NULL';
|
|
156
|
+
if (column.collation !== undefined) {
|
|
157
|
+
if (!['BINARY', 'NOCASE', 'RTRIM'].includes(column.collation)) fail('unsupported column collation');
|
|
158
|
+
out += ` COLLATE ${column.collation}`;
|
|
159
|
+
}
|
|
160
|
+
if (hasDefault) out += context.additive ? ` DEFAULT ${emitter.expr(column.default)}` : ` DEFAULT (${emitter.expr(column.default)})`;
|
|
161
|
+
if (column.check !== undefined) out += ` CHECK (${emitter.expr(column.check)})`;
|
|
162
|
+
if (column.generated !== undefined) {
|
|
163
|
+
if (hasDefault) fail('a generated column cannot have a default');
|
|
164
|
+
out += ` GENERATED ALWAYS AS (${emitter.expr(column.generated)}) ${column.stored ? 'STORED' : 'VIRTUAL'}`;
|
|
165
|
+
}
|
|
166
|
+
if (column.references !== undefined) {
|
|
167
|
+
if (column.references?.columns?.length !== 1) fail('a column reference requires one referenced column');
|
|
168
|
+
out += ` ${referenceSql(column.references)}`;
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Render one explicit main-schema operation; validation never executes SQL.
|
|
174
|
+
* @param {any} operation @returns {string} */
|
|
175
|
+
export function schemaChangeSql(operation) {
|
|
176
|
+
check(operation, ['op', 'table', 'column', 'name', 'to', 'ifExists'], 'schema change');
|
|
177
|
+
switch (operation.op) {
|
|
178
|
+
case 'addColumn':
|
|
179
|
+
check(operation, ['op', 'table', 'column'], 'addColumn');
|
|
180
|
+
return `ALTER TABLE "main".${q(operation.table)} ADD COLUMN ${columnSql(operation.column, { additive: true }, relationalEmitter({ inline: true }))}`;
|
|
181
|
+
case 'renameTable':
|
|
182
|
+
check(operation, ['op', 'table', 'to'], 'renameTable');
|
|
183
|
+
return `ALTER TABLE "main".${q(operation.table)} RENAME TO ${q(operation.to)}`;
|
|
184
|
+
case 'dropIndex': case 'dropTable': {
|
|
185
|
+
const index = operation.op === 'dropIndex';
|
|
186
|
+
check(operation, ['op', index ? 'name' : 'table', 'ifExists'], operation.op);
|
|
187
|
+
if (operation.ifExists !== undefined && typeof operation.ifExists !== 'boolean') fail('ifExists must be boolean');
|
|
188
|
+
return `DROP ${index ? 'INDEX' : 'TABLE'}${operation.ifExists ? ' IF EXISTS' : ''} "main".${q(index ? operation.name : operation.table)}`;
|
|
189
|
+
}
|
|
190
|
+
default: return fail('schema change is addColumn, dropIndex, renameTable or dropTable');
|
|
191
|
+
}
|
|
192
|
+
}
|