@visorcraft/mongreldb 0.18.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 +68 -0
- package/index.d.ts +31 -0
- package/index.js +70 -0
- package/mongreldb.linux-x64-gnu.node +0 -0
- package/native.d.ts +426 -0
- package/native.js +323 -0
- package/package.json +33 -0
- package/smoke.mjs +830 -0
package/smoke.mjs
ADDED
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
const { Database, ConditionKind, ColumnType, ConflictError } = require('./index.js');
|
|
4
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import assert from 'node:assert';
|
|
8
|
+
|
|
9
|
+
function makeTempDir() {
|
|
10
|
+
return mkdtempSync(join(tmpdir(), 'mongreldb-smoke-'));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// ── Multi-table API ───────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const dir1 = makeTempDir();
|
|
16
|
+
const db = Database.withPath(dir1);
|
|
17
|
+
|
|
18
|
+
// Create two tables.
|
|
19
|
+
const schemaA = {
|
|
20
|
+
columns: [
|
|
21
|
+
{ id: 1, name: 'id', ty: 1, primaryKey: true, nullable: false },
|
|
22
|
+
{ id: 2, name: 'v', ty: 1, primaryKey: false, nullable: false },
|
|
23
|
+
],
|
|
24
|
+
indexes: [],
|
|
25
|
+
};
|
|
26
|
+
const schemaB = {
|
|
27
|
+
columns: [
|
|
28
|
+
{ id: 1, name: 'id', ty: 1, primaryKey: true, nullable: false },
|
|
29
|
+
{ id: 2, name: 'tag', ty: 5, primaryKey: false, nullable: false },
|
|
30
|
+
],
|
|
31
|
+
indexes: [{ name: 'tag_idx', columnId: 2, kind: 0 }],
|
|
32
|
+
};
|
|
33
|
+
db.createTable('a', schemaA);
|
|
34
|
+
db.createTable('b', schemaB);
|
|
35
|
+
|
|
36
|
+
assert(db.tableNames().length === 2, 'two tables');
|
|
37
|
+
|
|
38
|
+
// Write to each table.
|
|
39
|
+
const tableA = db.getTable('a');
|
|
40
|
+
const tableB = db.getTable('b');
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < 100; i++) {
|
|
43
|
+
tableA.put([
|
|
44
|
+
{ columnId: 1, int64: BigInt(i) },
|
|
45
|
+
{ columnId: 2, int64: BigInt(i * 10) },
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
for (let i = 0; i < 50; i++) {
|
|
49
|
+
const tag = i % 2 === 0 ? 'even' : 'odd';
|
|
50
|
+
tableB.put([
|
|
51
|
+
{ columnId: 1, int64: BigInt(i) },
|
|
52
|
+
{ columnId: 2, bytes: Buffer.from(tag) },
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
tableA.commit();
|
|
57
|
+
tableB.commit();
|
|
58
|
+
|
|
59
|
+
assert(tableA.count() === 100n, `tableA count ${tableA.count()}`);
|
|
60
|
+
assert(tableB.count() === 50n, `tableB count ${tableB.count()}`);
|
|
61
|
+
|
|
62
|
+
// Point read.
|
|
63
|
+
const row = tableA.get(5n);
|
|
64
|
+
assert(row !== undefined, 'get returns a row');
|
|
65
|
+
assert(row.cells[0].int64 === 5n, `row id is 5, got ${row.cells[0].int64}`);
|
|
66
|
+
|
|
67
|
+
// Hybrid query on tableB.
|
|
68
|
+
const results = tableB.query([
|
|
69
|
+
{ kind: ConditionKind.BitmapEq, columnId: 2, text: 'even' },
|
|
70
|
+
]);
|
|
71
|
+
assert(results.length === 25, `25 even rows, got ${results.length}`);
|
|
72
|
+
|
|
73
|
+
// SQL query (cross-table).
|
|
74
|
+
const arrowBuf = await db.sql('SELECT COUNT(*) FROM a');
|
|
75
|
+
assert(arrowBuf.length > 0, 'SQL returns Arrow IPC bytes');
|
|
76
|
+
|
|
77
|
+
db.close();
|
|
78
|
+
rmSync(dir1, { recursive: true });
|
|
79
|
+
console.log('smoke: multi-table API ✓');
|
|
80
|
+
|
|
81
|
+
// ── Async variants ────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
const dir2 = makeTempDir();
|
|
84
|
+
const db2 = Database.withPath(dir2);
|
|
85
|
+
db2.createTable('t', schemaA);
|
|
86
|
+
const t = db2.getTable('t');
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < 50; i++) {
|
|
89
|
+
await t.putAsync([
|
|
90
|
+
{ columnId: 1, int64: BigInt(i) },
|
|
91
|
+
{ columnId: 2, int64: BigInt(i) },
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
await t.commitAsync();
|
|
95
|
+
const cnt = await t.countAsync();
|
|
96
|
+
assert(cnt === 50n, `async count ${cnt}`);
|
|
97
|
+
|
|
98
|
+
db2.close();
|
|
99
|
+
rmSync(dir2, { recursive: true });
|
|
100
|
+
console.log('smoke: async API ✓');
|
|
101
|
+
|
|
102
|
+
// ── Cross-table Transaction + ConflictError ───────────────────────────────
|
|
103
|
+
|
|
104
|
+
const dir3 = makeTempDir();
|
|
105
|
+
const db3 = Database.withPath(dir3);
|
|
106
|
+
db3.createTable('orders', {
|
|
107
|
+
columns: [
|
|
108
|
+
{ id: 1, name: 'id', ty: 1, primaryKey: true, nullable: false },
|
|
109
|
+
{ id: 2, name: 'amount', ty: 1, primaryKey: false, nullable: false },
|
|
110
|
+
],
|
|
111
|
+
indexes: [],
|
|
112
|
+
});
|
|
113
|
+
db3.createTable('customers', {
|
|
114
|
+
columns: [
|
|
115
|
+
{ id: 1, name: 'id', ty: 1, primaryKey: true, nullable: false },
|
|
116
|
+
{ id: 2, name: 'orders', ty: 1, primaryKey: false, nullable: false },
|
|
117
|
+
],
|
|
118
|
+
indexes: [],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Atomic cross-table transaction.
|
|
122
|
+
const { Transaction } = require('./index.js');
|
|
123
|
+
const tx = new Transaction(db3);
|
|
124
|
+
tx.put('orders', [
|
|
125
|
+
{ columnId: 1, int64: 1n },
|
|
126
|
+
{ columnId: 2, int64: 100n },
|
|
127
|
+
]);
|
|
128
|
+
tx.put('customers', [
|
|
129
|
+
{ columnId: 1, int64: 1n },
|
|
130
|
+
{ columnId: 2, int64: 1n },
|
|
131
|
+
]);
|
|
132
|
+
const epoch = tx.commit();
|
|
133
|
+
assert(typeof epoch === 'bigint', `epoch is bigint, got ${typeof epoch}`);
|
|
134
|
+
|
|
135
|
+
assert(db3.getTable('orders').count() === 1n, 'order committed');
|
|
136
|
+
assert(db3.getTable('customers').count() === 1n, 'customer committed');
|
|
137
|
+
|
|
138
|
+
// Rollback test.
|
|
139
|
+
const tx2 = new Transaction(db3);
|
|
140
|
+
tx2.put('orders', [
|
|
141
|
+
{ columnId: 1, int64: 2n },
|
|
142
|
+
{ columnId: 2, int64: 200n },
|
|
143
|
+
]);
|
|
144
|
+
tx2.rollback();
|
|
145
|
+
assert(db3.getTable('orders').count() === 1n, 'rollback leaves 1 row');
|
|
146
|
+
|
|
147
|
+
db3.close();
|
|
148
|
+
rmSync(dir3, { recursive: true });
|
|
149
|
+
console.log('smoke: cross-table Transaction ✓');
|
|
150
|
+
|
|
151
|
+
// ── WriteBuffer from Table + atomic visibility ─────────────────────────────
|
|
152
|
+
|
|
153
|
+
const dir4 = makeTempDir();
|
|
154
|
+
const db4 = Database.withPath(dir4);
|
|
155
|
+
db4.createTable('w', {
|
|
156
|
+
columns: [
|
|
157
|
+
{ id: 1, name: 'id', ty: 1, primaryKey: true, nullable: false },
|
|
158
|
+
{ id: 2, name: 'v', ty: 1, primaryKey: false, nullable: false },
|
|
159
|
+
],
|
|
160
|
+
indexes: [],
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const wTable = db4.getTable('w');
|
|
164
|
+
const { WriteBuffer } = require('./index.js');
|
|
165
|
+
const wb = new WriteBuffer(wTable, 10);
|
|
166
|
+
for (let i = 0; i < 25; i++) {
|
|
167
|
+
wb.put([
|
|
168
|
+
{ columnId: 1, int64: BigInt(i) },
|
|
169
|
+
{ columnId: 2, int64: BigInt(i * 2) },
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
wb.flush();
|
|
173
|
+
assert(wTable.count() === 25n, `writebuffer count ${wTable.count()}`);
|
|
174
|
+
|
|
175
|
+
// Atomic cross-table visibility: a transaction's writes are not visible until commit.
|
|
176
|
+
db4.createTable('vis', {
|
|
177
|
+
columns: [
|
|
178
|
+
{ id: 1, name: 'id', ty: 1, primaryKey: true, nullable: false },
|
|
179
|
+
{ id: 2, name: 'v', ty: 1, primaryKey: false, nullable: false },
|
|
180
|
+
],
|
|
181
|
+
indexes: [],
|
|
182
|
+
});
|
|
183
|
+
const tx3 = new Transaction(db4);
|
|
184
|
+
tx3.put('vis', [{ columnId: 1, int64: 1n }, { columnId: 2, int64: 42n }]);
|
|
185
|
+
// Before commit: vis table has 0 rows.
|
|
186
|
+
assert(db4.getTable('vis').count() === 0n, 'pre-commit: 0 rows');
|
|
187
|
+
tx3.commit();
|
|
188
|
+
// After commit: vis table has 1 row.
|
|
189
|
+
assert(db4.getTable('vis').count() === 1n, 'post-commit: 1 row');
|
|
190
|
+
|
|
191
|
+
db4.close();
|
|
192
|
+
rmSync(dir4, { recursive: true });
|
|
193
|
+
console.log('smoke: WriteBuffer + atomic visibility ✓');
|
|
194
|
+
|
|
195
|
+
// ── New surface: putBatch / bulkLoadTyped / queryArrow / begin / async ──────
|
|
196
|
+
|
|
197
|
+
const dir5 = makeTempDir();
|
|
198
|
+
const db5 = Database.withPath(dir5);
|
|
199
|
+
db5.createTable('nums', {
|
|
200
|
+
columns: [
|
|
201
|
+
{ id: 1, name: 'id', ty: 1, primaryKey: true, nullable: false },
|
|
202
|
+
{ id: 2, name: 'v', ty: 1, primaryKey: false, nullable: false },
|
|
203
|
+
],
|
|
204
|
+
indexes: [{ name: 'v_idx', columnId: 2, kind: 0 }],
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const nums = db5.table('nums'); // table() alias for getTable()
|
|
208
|
+
const epoch0 = db5.snapshotEpoch();
|
|
209
|
+
assert(typeof epoch0 === 'bigint', 'snapshotEpoch is bigint');
|
|
210
|
+
|
|
211
|
+
// putBatch: three rows in one call → three row ids.
|
|
212
|
+
const batchIds = nums.putBatch([
|
|
213
|
+
[{ columnId: 1, int64: 1n }, { columnId: 2, int64: 7n }],
|
|
214
|
+
[{ columnId: 1, int64: 2n }, { columnId: 2, int64: 7n }],
|
|
215
|
+
[{ columnId: 1, int64: 3n }, { columnId: 2, int64: 9n }],
|
|
216
|
+
]);
|
|
217
|
+
assert(batchIds.length === 3, `putBatch returns 3 ids, got ${batchIds.length}`);
|
|
218
|
+
await nums.flushAsync(); // flush to a sorted run (also exercises flushAsync)
|
|
219
|
+
assert(nums.count() === 3n, `putBatch count ${nums.count()}`);
|
|
220
|
+
|
|
221
|
+
// queryArrow: matching rows as Arrow IPC bytes — verify the IPC file magic.
|
|
222
|
+
const arrow = nums.queryArrow([
|
|
223
|
+
{ kind: ConditionKind.RangeInt, columnId: 2, int64Lo: 7n, int64Hi: 7n },
|
|
224
|
+
]);
|
|
225
|
+
assert(arrow.length > 0, 'queryArrow returns bytes');
|
|
226
|
+
assert(arrow.subarray(0, 6).toString('ascii') === 'ARROW1', 'queryArrow emits Arrow IPC');
|
|
227
|
+
|
|
228
|
+
// bulkLoadTyped: typed Int64 columns straight from JS BigInt64Array buffers.
|
|
229
|
+
db5.createTable('bulk', {
|
|
230
|
+
columns: [
|
|
231
|
+
{ id: 1, name: 'id', ty: 1, primaryKey: true, nullable: false },
|
|
232
|
+
{ id: 2, name: 'v', ty: 1, primaryKey: false, nullable: false },
|
|
233
|
+
],
|
|
234
|
+
indexes: [],
|
|
235
|
+
});
|
|
236
|
+
const bulk = db5.table('bulk');
|
|
237
|
+
const toBuf = (a) => Buffer.from(a.buffer, a.byteOffset, a.byteLength);
|
|
238
|
+
const bulkEpoch = bulk.bulkLoadTyped([
|
|
239
|
+
{ columnId: 1, ty: ColumnType.Int64, data: toBuf(new BigInt64Array([10n, 11n, 12n])) },
|
|
240
|
+
{ columnId: 2, ty: ColumnType.Int64, data: toBuf(new BigInt64Array([100n, 110n, 120n])) },
|
|
241
|
+
]);
|
|
242
|
+
assert(typeof bulkEpoch === 'bigint', 'bulkLoadTyped returns an epoch');
|
|
243
|
+
assert(bulk.count() === 3n, `bulkLoadTyped count ${bulk.count()}`);
|
|
244
|
+
|
|
245
|
+
// db.begin() factory + commitAsync().
|
|
246
|
+
const tx5 = db5.begin();
|
|
247
|
+
tx5.put('nums', [{ columnId: 1, int64: 99n }, { columnId: 2, int64: 5n }]);
|
|
248
|
+
const txEpoch = await tx5.commitAsync();
|
|
249
|
+
assert(typeof txEpoch === 'bigint', 'commitAsync returns an epoch');
|
|
250
|
+
assert(nums.count() === 4n, `after txn count ${nums.count()}`);
|
|
251
|
+
assert(db5.snapshotEpoch() >= epoch0, 'snapshotEpoch advances');
|
|
252
|
+
|
|
253
|
+
db5.close();
|
|
254
|
+
rmSync(dir5, { recursive: true });
|
|
255
|
+
console.log('smoke: putBatch / bulkLoadTyped / queryArrow / begin / async ✓');
|
|
256
|
+
|
|
257
|
+
// ── Encrypted database round-trip (encryption ships on by default) ──────────
|
|
258
|
+
|
|
259
|
+
const dir6 = makeTempDir();
|
|
260
|
+
const PASS = 'qa-verify-passphrase';
|
|
261
|
+
const secretSchema = {
|
|
262
|
+
columns: [
|
|
263
|
+
{ id: 1, name: 'id', ty: 1, primaryKey: true, nullable: false },
|
|
264
|
+
{ id: 2, name: 'v', ty: 1, primaryKey: false, nullable: false },
|
|
265
|
+
],
|
|
266
|
+
indexes: [],
|
|
267
|
+
};
|
|
268
|
+
{
|
|
269
|
+
const edb = Database.createEncrypted(dir6, PASS);
|
|
270
|
+
edb.createTable('secret', secretSchema);
|
|
271
|
+
const st = edb.getTable('secret');
|
|
272
|
+
st.put([{ columnId: 1, int64: 7n }, { columnId: 2, int64: 42n }]);
|
|
273
|
+
st.flush();
|
|
274
|
+
edb.close();
|
|
275
|
+
}
|
|
276
|
+
// Reopen with the correct passphrase → data is readable.
|
|
277
|
+
{
|
|
278
|
+
const edb = Database.openEncrypted(dir6, PASS);
|
|
279
|
+
assert(edb.getTable('secret').count() === 1n, 'encrypted reopen sees the row');
|
|
280
|
+
const r = edb.getTable('secret').get(0n);
|
|
281
|
+
assert(r !== undefined && r.cells[1].int64 === 42n, 'encrypted value round-trips');
|
|
282
|
+
edb.close();
|
|
283
|
+
}
|
|
284
|
+
// Wrong passphrase is rejected.
|
|
285
|
+
assert.throws(() => Database.openEncrypted(dir6, 'wrong-passphrase'), 'wrong passphrase rejected');
|
|
286
|
+
rmSync(dir6, { recursive: true });
|
|
287
|
+
console.log('smoke: encrypted round-trip ✓');
|
|
288
|
+
|
|
289
|
+
// ── TxnTable sub-API: tx.table(name).put/delete — additive, backwards compatible ──
|
|
290
|
+
{
|
|
291
|
+
const dir7 = makeTempDir();
|
|
292
|
+
const db7 = Database.withPath(dir7);
|
|
293
|
+
const sch = {
|
|
294
|
+
columns: [
|
|
295
|
+
{ id: 1, name: 'id', ty: 1, primaryKey: true, nullable: false },
|
|
296
|
+
{ id: 2, name: 'v', ty: 1, primaryKey: false, nullable: false },
|
|
297
|
+
],
|
|
298
|
+
indexes: [],
|
|
299
|
+
};
|
|
300
|
+
db7.createTable('a', sch);
|
|
301
|
+
db7.createTable('b', sch);
|
|
302
|
+
|
|
303
|
+
// Scope ops to a table once; put/putBatch stage into ONE transaction.
|
|
304
|
+
const tx = db7.begin();
|
|
305
|
+
const ta = tx.table('a');
|
|
306
|
+
ta.put([{ columnId: 1, int64: 1n }, { columnId: 2, int64: 10n }]);
|
|
307
|
+
ta.putBatch([
|
|
308
|
+
[{ columnId: 1, int64: 2n }, { columnId: 2, int64: 20n }],
|
|
309
|
+
[{ columnId: 1, int64: 3n }, { columnId: 2, int64: 30n }],
|
|
310
|
+
]);
|
|
311
|
+
tx.table('b').put([{ columnId: 1, int64: 1n }, { columnId: 2, int64: 99n }]);
|
|
312
|
+
// Atomic: nothing visible until the parent transaction commits.
|
|
313
|
+
assert(db7.getTable('a').count() === 0n, 'pre-commit a: 0');
|
|
314
|
+
tx.commit();
|
|
315
|
+
assert(db7.getTable('a').count() === 3n, `a after commit: ${db7.getTable('a').count()}`);
|
|
316
|
+
assert(db7.getTable('b').count() === 1n, `b after commit: ${db7.getTable('b').count()}`);
|
|
317
|
+
|
|
318
|
+
// Flat API still works alongside the sub-API (backwards compatible).
|
|
319
|
+
const tx2 = db7.begin();
|
|
320
|
+
tx2.put('a', [{ columnId: 1, int64: 4n }, { columnId: 2, int64: 40n }]); // flat
|
|
321
|
+
tx2.table('b').put([{ columnId: 1, int64: 2n }, { columnId: 2, int64: 88n }]); // sub-API
|
|
322
|
+
tx2.commit();
|
|
323
|
+
assert(db7.getTable('a').count() === 4n, `flat add: ${db7.getTable('a').count()}`);
|
|
324
|
+
assert(db7.getTable('b').count() === 2n, `sub add: ${db7.getTable('b').count()}`);
|
|
325
|
+
|
|
326
|
+
// Sub-API delete: capture a real row id via a direct put, then delete it.
|
|
327
|
+
const rid = db7.getTable('a').put([{ columnId: 1, int64: 100n }, { columnId: 2, int64: 1n }]).rowId;
|
|
328
|
+
db7.getTable('a').commit();
|
|
329
|
+
const before = db7.getTable('a').count();
|
|
330
|
+
const txDel = db7.begin();
|
|
331
|
+
txDel.table('a').delete(rid);
|
|
332
|
+
txDel.commit();
|
|
333
|
+
assert(db7.getTable('a').count() === before - 1n, 'sub-API delete removed the row');
|
|
334
|
+
|
|
335
|
+
db7.close();
|
|
336
|
+
rmSync(dir7, { recursive: true });
|
|
337
|
+
}
|
|
338
|
+
console.log('smoke: TxnTable sub-API ✓');
|
|
339
|
+
|
|
340
|
+
// ── Full-range Int64 / BigInt round-trip ───────────────────────────────────
|
|
341
|
+
{
|
|
342
|
+
const dir8 = makeTempDir();
|
|
343
|
+
const db8 = Database.withPath(dir8);
|
|
344
|
+
const i64Schema = {
|
|
345
|
+
columns: [
|
|
346
|
+
{ id: 1, name: 'id', ty: 1, primaryKey: true, nullable: false },
|
|
347
|
+
{ id: 2, name: 'value', ty: 1, primaryKey: false, nullable: false },
|
|
348
|
+
],
|
|
349
|
+
indexes: [{ name: 'value_idx', columnId: 2, kind: 0 }],
|
|
350
|
+
};
|
|
351
|
+
db8.createTable('i64', i64Schema);
|
|
352
|
+
const i64 = db8.getTable('i64');
|
|
353
|
+
|
|
354
|
+
const max = 9_223_372_036_854_775_807n;
|
|
355
|
+
const min = -9_223_372_036_854_775_808n;
|
|
356
|
+
const ridMax = i64.put([
|
|
357
|
+
{ columnId: 1, int64: 1n },
|
|
358
|
+
{ columnId: 2, int64: max },
|
|
359
|
+
]).rowId;
|
|
360
|
+
const ridMin = i64.put([
|
|
361
|
+
{ columnId: 1, int64: 2n },
|
|
362
|
+
{ columnId: 2, int64: min },
|
|
363
|
+
]).rowId;
|
|
364
|
+
i64.commit();
|
|
365
|
+
|
|
366
|
+
const rMax = i64.get(ridMax);
|
|
367
|
+
const rMin = i64.get(ridMin);
|
|
368
|
+
assert(rMax !== undefined, 'max row read');
|
|
369
|
+
assert(rMin !== undefined, 'min row read');
|
|
370
|
+
assert(rMax.cells[1].int64 === max, `max round-trip: ${rMax.cells[1].int64}`);
|
|
371
|
+
assert(rMin.cells[1].int64 === min, `min round-trip: ${rMin.cells[1].int64}`);
|
|
372
|
+
|
|
373
|
+
// RangeInt query with BigInt bounds.
|
|
374
|
+
const range = i64.query([
|
|
375
|
+
{ kind: ConditionKind.RangeInt, columnId: 2, int64Lo: min, int64Hi: 0n },
|
|
376
|
+
]);
|
|
377
|
+
assert(range.length === 1, `one row in negative range, got ${range.length}`);
|
|
378
|
+
assert(range[0].cells[1].int64 === min, 'RangeInt returns the negative min');
|
|
379
|
+
|
|
380
|
+
const all = i64.query([
|
|
381
|
+
{ kind: ConditionKind.RangeInt, columnId: 2, int64Lo: min, int64Hi: max },
|
|
382
|
+
]);
|
|
383
|
+
assert(all.length === 2, `both rows in full int64 range, got ${all.length}`);
|
|
384
|
+
|
|
385
|
+
db8.close();
|
|
386
|
+
rmSync(dir8, { recursive: true });
|
|
387
|
+
}
|
|
388
|
+
console.log('smoke: full-range Int64 / BigInt ✓');
|
|
389
|
+
|
|
390
|
+
// ── Typed primary-key get/delete ───────────────────────────────────────────
|
|
391
|
+
{
|
|
392
|
+
const dir9 = makeTempDir();
|
|
393
|
+
const db9 = Database.withPath(dir9);
|
|
394
|
+
|
|
395
|
+
// Text primary-key table.
|
|
396
|
+
db9.createTable('text_pk', {
|
|
397
|
+
columns: [
|
|
398
|
+
{ id: 1, name: 'id', ty: 5, primaryKey: true, nullable: false },
|
|
399
|
+
{ id: 2, name: 'v', ty: 1, primaryKey: false, nullable: false },
|
|
400
|
+
],
|
|
401
|
+
indexes: [],
|
|
402
|
+
});
|
|
403
|
+
const textPk = db9.getTable('text_pk');
|
|
404
|
+
textPk.put([
|
|
405
|
+
{ columnId: 1, text: 'alpha' },
|
|
406
|
+
{ columnId: 2, int64: 100n },
|
|
407
|
+
]);
|
|
408
|
+
textPk.put([
|
|
409
|
+
{ columnId: 1, text: 'beta' },
|
|
410
|
+
{ columnId: 2, int64: 200n },
|
|
411
|
+
]);
|
|
412
|
+
textPk.commit();
|
|
413
|
+
|
|
414
|
+
const gotAlpha = textPk.getByPkText('alpha');
|
|
415
|
+
assert(gotAlpha !== null, 'getByPkText finds alpha');
|
|
416
|
+
assert(gotAlpha.cells[1].int64 === 100n, 'alpha value round-trips');
|
|
417
|
+
|
|
418
|
+
textPk.deleteByPkText('alpha');
|
|
419
|
+
textPk.commit();
|
|
420
|
+
assert(textPk.getByPkText('alpha') === null, 'alpha deleted');
|
|
421
|
+
assert(textPk.getByPkText('beta') !== null, 'beta remains');
|
|
422
|
+
|
|
423
|
+
// Int64 primary-key table.
|
|
424
|
+
db9.createTable('int64_pk', {
|
|
425
|
+
columns: [
|
|
426
|
+
{ id: 1, name: 'id', ty: 1, primaryKey: true, nullable: false },
|
|
427
|
+
{ id: 2, name: 'v', ty: 5, primaryKey: false, nullable: false },
|
|
428
|
+
],
|
|
429
|
+
indexes: [],
|
|
430
|
+
});
|
|
431
|
+
const int64Pk = db9.getTable('int64_pk');
|
|
432
|
+
int64Pk.put([
|
|
433
|
+
{ columnId: 1, int64: 42n },
|
|
434
|
+
{ columnId: 2, text: 'forty-two' },
|
|
435
|
+
]);
|
|
436
|
+
int64Pk.put([
|
|
437
|
+
{ columnId: 1, int64: -7n },
|
|
438
|
+
{ columnId: 2, text: 'negative-seven' },
|
|
439
|
+
]);
|
|
440
|
+
int64Pk.commit();
|
|
441
|
+
|
|
442
|
+
const got42 = int64Pk.getByPkInt64(42n);
|
|
443
|
+
assert(got42 !== null, 'getByPkInt64 finds 42');
|
|
444
|
+
assert(got42.cells[1].text === 'forty-two', '42 value round-trips');
|
|
445
|
+
|
|
446
|
+
const gotNeg = int64Pk.getByPkInt64(-7n);
|
|
447
|
+
assert(gotNeg !== null, 'getByPkInt64 finds -7');
|
|
448
|
+
|
|
449
|
+
int64Pk.deleteByPkInt64(42n);
|
|
450
|
+
int64Pk.commit();
|
|
451
|
+
assert(int64Pk.getByPkInt64(42n) === null, '42 deleted');
|
|
452
|
+
assert(int64Pk.getByPkInt64(-7n) !== null, '-7 remains');
|
|
453
|
+
|
|
454
|
+
// Direct row-id delete via TableHandle.delete.
|
|
455
|
+
const rid = textPk.put([
|
|
456
|
+
{ columnId: 1, text: 'gamma' },
|
|
457
|
+
{ columnId: 2, int64: 300n },
|
|
458
|
+
]).rowId;
|
|
459
|
+
textPk.commit();
|
|
460
|
+
assert(textPk.get(rid) !== null, 'gamma inserted');
|
|
461
|
+
textPk.delete(rid);
|
|
462
|
+
textPk.commit();
|
|
463
|
+
assert(textPk.get(rid) === null, 'gamma deleted by row id');
|
|
464
|
+
|
|
465
|
+
db9.close();
|
|
466
|
+
rmSync(dir9, { recursive: true });
|
|
467
|
+
}
|
|
468
|
+
console.log('smoke: typed primary-key get/delete ✓');
|
|
469
|
+
|
|
470
|
+
// ── A3: catalog-aware addColumn ────────────────────────────────────────────
|
|
471
|
+
{
|
|
472
|
+
const dirA3 = makeTempDir();
|
|
473
|
+
const dbA3 = Database.withPath(dirA3);
|
|
474
|
+
dbA3.createTable('evolve', {
|
|
475
|
+
columns: [
|
|
476
|
+
{ id: 1, name: 'id', ty: ColumnType.Int64, primaryKey: true, nullable: false },
|
|
477
|
+
{ id: 2, name: 'v', ty: ColumnType.Int64, primaryKey: false, nullable: false },
|
|
478
|
+
],
|
|
479
|
+
indexes: [],
|
|
480
|
+
});
|
|
481
|
+
const evolve = dbA3.getTable('evolve');
|
|
482
|
+
evolve.put([
|
|
483
|
+
{ columnId: 1, int64: 1n },
|
|
484
|
+
{ columnId: 2, int64: 100n },
|
|
485
|
+
]);
|
|
486
|
+
evolve.commit();
|
|
487
|
+
|
|
488
|
+
// Adding a non-null column without a default must be rejected.
|
|
489
|
+
assert.throws(
|
|
490
|
+
() =>
|
|
491
|
+
dbA3.addColumn('evolve', {
|
|
492
|
+
id: 3,
|
|
493
|
+
name: 'missing_default',
|
|
494
|
+
ty: ColumnType.Int64,
|
|
495
|
+
primaryKey: false,
|
|
496
|
+
nullable: false,
|
|
497
|
+
}),
|
|
498
|
+
/non-null column added without default/
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
// Add a nullable Int64 column.
|
|
502
|
+
const newColId = dbA3.addColumn('evolve', {
|
|
503
|
+
id: 3,
|
|
504
|
+
name: 'extra',
|
|
505
|
+
ty: ColumnType.Int64,
|
|
506
|
+
primaryKey: false,
|
|
507
|
+
nullable: true,
|
|
508
|
+
});
|
|
509
|
+
assert(typeof newColId === 'bigint', 'addColumn returns a column id');
|
|
510
|
+
|
|
511
|
+
// Existing row reads back with null in the new column.
|
|
512
|
+
const oldRow = evolve.get(0n);
|
|
513
|
+
assert(oldRow !== null, 'old row still readable');
|
|
514
|
+
const extraCell = oldRow.cells.find((c) => c.columnId === 3);
|
|
515
|
+
assert(extraCell !== undefined, 'new column present in row');
|
|
516
|
+
assert(extraCell.int64 === undefined || extraCell.int64 === null, 'new column is null');
|
|
517
|
+
|
|
518
|
+
// Rename the column through native ALTER COLUMN while preserving the stable id.
|
|
519
|
+
const alteredColId = dbA3.alterColumn('evolve', 'extra', {
|
|
520
|
+
id: 3,
|
|
521
|
+
name: 'renamed_extra',
|
|
522
|
+
ty: ColumnType.Int64,
|
|
523
|
+
primaryKey: false,
|
|
524
|
+
nullable: true,
|
|
525
|
+
});
|
|
526
|
+
assert(alteredColId === newColId, 'alterColumn returns the stable column id');
|
|
527
|
+
const evolvedColumns = dbA3.tableColumns('evolve');
|
|
528
|
+
assert(evolvedColumns.includes('renamed_extra'), 'alterColumn renamed the column');
|
|
529
|
+
assert(!evolvedColumns.includes('extra'), 'old column name removed after alterColumn');
|
|
530
|
+
|
|
531
|
+
dbA3.close();
|
|
532
|
+
rmSync(dirA3, { recursive: true });
|
|
533
|
+
}
|
|
534
|
+
console.log('smoke: A3 catalog-aware addColumn/alterColumn ✓');
|
|
535
|
+
|
|
536
|
+
// ── A4: backup and integrity primitives ────────────────────────────────────
|
|
537
|
+
{
|
|
538
|
+
const dirA4 = makeTempDir();
|
|
539
|
+
const dbA4 = Database.withPath(dirA4);
|
|
540
|
+
dbA4.createTable('check_t', {
|
|
541
|
+
columns: [
|
|
542
|
+
{ id: 1, name: 'id', ty: ColumnType.Int64, primaryKey: true, nullable: false },
|
|
543
|
+
],
|
|
544
|
+
indexes: [],
|
|
545
|
+
});
|
|
546
|
+
dbA4.getTable('check_t').put([{ columnId: 1, int64: 1n }]);
|
|
547
|
+
dbA4.getTable('check_t').commit();
|
|
548
|
+
|
|
549
|
+
const checkJson = dbA4.check();
|
|
550
|
+
const checkReport = JSON.parse(checkJson);
|
|
551
|
+
assert(checkReport.ok === true, 'check reports ok on fresh db');
|
|
552
|
+
assert(Array.isArray(checkReport.tables), 'check report has tables array');
|
|
553
|
+
|
|
554
|
+
const doctorJson = dbA4.doctor();
|
|
555
|
+
const doctorReport = JSON.parse(doctorJson);
|
|
556
|
+
assert(doctorReport.ok === true, 'doctor reports ok on fresh db');
|
|
557
|
+
assert(Array.isArray(doctorReport.quarantined), 'doctor report has quarantined array');
|
|
558
|
+
|
|
559
|
+
assert(dbA4.directory() === dirA4, 'directory returns the creation path');
|
|
560
|
+
|
|
561
|
+
dbA4.close();
|
|
562
|
+
rmSync(dirA4, { recursive: true });
|
|
563
|
+
}
|
|
564
|
+
console.log('smoke: A4 check / doctor / directory ✓');
|
|
565
|
+
|
|
566
|
+
// ── A5: ConflictError + transaction(fn) retry wrapper ──────────────────────
|
|
567
|
+
{
|
|
568
|
+
const dirA5 = makeTempDir();
|
|
569
|
+
const dbA5 = Database.withPath(dirA5);
|
|
570
|
+
dbA5.createTable('retry', {
|
|
571
|
+
columns: [
|
|
572
|
+
{ id: 1, name: 'id', ty: ColumnType.Int64, primaryKey: true, nullable: false },
|
|
573
|
+
{ id: 2, name: 'v', ty: ColumnType.Int64, primaryKey: false, nullable: false },
|
|
574
|
+
],
|
|
575
|
+
indexes: [],
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// Successful transaction helper run commits the staged write.
|
|
579
|
+
const epoch = await dbA5.transaction((txn) => {
|
|
580
|
+
txn.put('retry', [
|
|
581
|
+
{ columnId: 1, int64: 1n },
|
|
582
|
+
{ columnId: 2, int64: 42n },
|
|
583
|
+
]);
|
|
584
|
+
});
|
|
585
|
+
assert(typeof epoch === 'bigint', 'transaction helper returns epoch');
|
|
586
|
+
assert(dbA5.getTable('retry').count() === 1n, 'transaction helper committed the write');
|
|
587
|
+
|
|
588
|
+
// Non-conflict errors are re-thrown immediately.
|
|
589
|
+
let threw = false;
|
|
590
|
+
try {
|
|
591
|
+
await dbA5.transaction(() => {
|
|
592
|
+
throw new Error('boom');
|
|
593
|
+
});
|
|
594
|
+
} catch (e) {
|
|
595
|
+
threw = true;
|
|
596
|
+
assert(e.message === 'boom', 'non-conflict error is re-thrown');
|
|
597
|
+
}
|
|
598
|
+
assert(threw, 'non-conflict error propagated');
|
|
599
|
+
|
|
600
|
+
// ConflictError class is exported.
|
|
601
|
+
assert(new ConflictError('x') instanceof Error, 'ConflictError extends Error');
|
|
602
|
+
|
|
603
|
+
dbA5.close();
|
|
604
|
+
rmSync(dirA5, { recursive: true });
|
|
605
|
+
}
|
|
606
|
+
console.log('smoke: A5 ConflictError + transaction wrapper ✓');
|
|
607
|
+
|
|
608
|
+
// ── BigInt range validation ────────────────────────────────────────────────
|
|
609
|
+
{
|
|
610
|
+
const dirRange = makeTempDir();
|
|
611
|
+
const dbRange = Database.withPath(dirRange);
|
|
612
|
+
dbRange.createTable('range', {
|
|
613
|
+
columns: [
|
|
614
|
+
{ id: 1, name: 'id', ty: ColumnType.Int64, primaryKey: true, nullable: false },
|
|
615
|
+
{ id: 2, name: 'v', ty: ColumnType.Int64, primaryKey: false, nullable: false },
|
|
616
|
+
],
|
|
617
|
+
indexes: [{ name: 'v_idx', columnId: 2, kind: 0 }],
|
|
618
|
+
});
|
|
619
|
+
const range = dbRange.getTable('range');
|
|
620
|
+
range.put([
|
|
621
|
+
{ columnId: 1, int64: 1n },
|
|
622
|
+
{ columnId: 2, int64: 1n },
|
|
623
|
+
]);
|
|
624
|
+
range.commit();
|
|
625
|
+
const rid = range.get(0n).rowId;
|
|
626
|
+
|
|
627
|
+
const i64Max = 9_223_372_036_854_775_807n;
|
|
628
|
+
const i64Over = i64Max + 1n;
|
|
629
|
+
const u64Max = 18_446_744_073_709_551_615n;
|
|
630
|
+
|
|
631
|
+
// Cell value out of i64 range.
|
|
632
|
+
assert.throws(
|
|
633
|
+
() => range.put([{ columnId: 1, int64: i64Over }]),
|
|
634
|
+
/BigInt out of i64 range/,
|
|
635
|
+
'put rejects out-of-range i64 cell'
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
// RangeInt bounds out of i64 range.
|
|
639
|
+
assert.throws(
|
|
640
|
+
() =>
|
|
641
|
+
range.query([
|
|
642
|
+
{ kind: ConditionKind.RangeInt, columnId: 2, int64Lo: i64Over, int64Hi: i64Over },
|
|
643
|
+
]),
|
|
644
|
+
/BigInt out of i64 range/,
|
|
645
|
+
'RangeInt rejects out-of-range bound'
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
// Int64 primary-key lookup out of i64 range.
|
|
649
|
+
assert.throws(
|
|
650
|
+
() => range.getByPkInt64(i64Over),
|
|
651
|
+
/BigInt out of i64 range/,
|
|
652
|
+
'getByPkInt64 rejects out-of-range pk'
|
|
653
|
+
);
|
|
654
|
+
assert.throws(
|
|
655
|
+
() => range.deleteByPkInt64(i64Over),
|
|
656
|
+
/BigInt out of i64 range/,
|
|
657
|
+
'deleteByPkInt64 rejects out-of-range pk'
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
// Row ids are u64; negative or too-large values are rejected.
|
|
661
|
+
assert.throws(() => range.get(-1n), /BigInt out of u64 range/, 'get rejects negative row id');
|
|
662
|
+
assert.throws(() => range.delete(-1n), /BigInt out of u64 range/, 'delete rejects negative row id');
|
|
663
|
+
assert.throws(
|
|
664
|
+
() => range.get(u64Max + 1n),
|
|
665
|
+
/BigInt out of u64 range/,
|
|
666
|
+
'get rejects too-large row id'
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
// Transaction delete also validates the row id.
|
|
670
|
+
const txRange = dbRange.begin();
|
|
671
|
+
assert.throws(
|
|
672
|
+
() => txRange.delete('range', -1n),
|
|
673
|
+
/BigInt out of u64 range/,
|
|
674
|
+
'transaction delete rejects negative row id'
|
|
675
|
+
);
|
|
676
|
+
txRange.rollback();
|
|
677
|
+
|
|
678
|
+
dbRange.close();
|
|
679
|
+
rmSync(dirRange, { recursive: true });
|
|
680
|
+
}
|
|
681
|
+
console.log('smoke: BigInt range validation ✓');
|
|
682
|
+
|
|
683
|
+
// ── Engine-native AUTO_INCREMENT ──────────────────────────────────────────
|
|
684
|
+
{
|
|
685
|
+
const dirAi = makeTempDir();
|
|
686
|
+
const ai = Database.withPath(dirAi);
|
|
687
|
+
ai.createTable('things', {
|
|
688
|
+
columns: [
|
|
689
|
+
{ id: 1, name: 'id', ty: 1, primaryKey: true, nullable: false, autoIncrement: true },
|
|
690
|
+
{ id: 2, name: 'label', ty: 5, primaryKey: false, nullable: false },
|
|
691
|
+
],
|
|
692
|
+
indexes: [],
|
|
693
|
+
});
|
|
694
|
+
const t = ai.getTable('things');
|
|
695
|
+
|
|
696
|
+
// Omit the PK cell → engine assigns 1, 2, 3.
|
|
697
|
+
const r1 = t.put([{ columnId: 2, bytes: Buffer.from('a') }]);
|
|
698
|
+
const r2 = t.put([{ columnId: 2, bytes: Buffer.from('b') }]);
|
|
699
|
+
const r3 = t.put([{ columnId: 2, bytes: Buffer.from('c') }]);
|
|
700
|
+
assert(typeof r1.rowId === 'bigint', 'put returns rowId bigint');
|
|
701
|
+
assert(r1.autoInc === 1n, `omit pk → engine assigns 1 (got ${r1.autoInc})`);
|
|
702
|
+
assert(r2.autoInc === 2n, `omit pk → engine assigns 2`);
|
|
703
|
+
assert(r3.autoInc === 3n, `omit pk → engine assigns 3`);
|
|
704
|
+
assert(r1.rowId !== r2.rowId && r2.rowId !== r3.rowId, 'distinct row ids');
|
|
705
|
+
|
|
706
|
+
// Explicit id advances the counter past it.
|
|
707
|
+
const r4 = t.put([
|
|
708
|
+
{ columnId: 1, int64: 50n },
|
|
709
|
+
{ columnId: 2, bytes: Buffer.from('big') },
|
|
710
|
+
]);
|
|
711
|
+
assert(r4.autoInc == null, 'explicit id → autoInc null/undefined');
|
|
712
|
+
const r5 = t.put([{ columnId: 2, bytes: Buffer.from('n') }]);
|
|
713
|
+
assert(r5.autoInc === 51n, `omit after explicit 50 → 51 (got ${r5.autoInc})`);
|
|
714
|
+
t.commit();
|
|
715
|
+
|
|
716
|
+
// The assigned value is materialized in the row: each PK is retrievable.
|
|
717
|
+
for (const id of [1n, 2n, 3n, 50n, 51n]) {
|
|
718
|
+
const got = t.getByPkInt64(id);
|
|
719
|
+
assert(got !== null, `materialized pk ${id} retrievable`);
|
|
720
|
+
}
|
|
721
|
+
assert(t.getByPkInt64(4n) === null, 'id 4 was never assigned');
|
|
722
|
+
|
|
723
|
+
// Batch assigns per-row and returns each.
|
|
724
|
+
const batch = t.putBatch([
|
|
725
|
+
[{ columnId: 2, bytes: Buffer.from('x') }],
|
|
726
|
+
[{ columnId: 2, bytes: Buffer.from('y') }],
|
|
727
|
+
]);
|
|
728
|
+
assert.deepEqual(batch.map((b) => b.autoInc), [52n, 53n], 'batch per-row ids');
|
|
729
|
+
t.commit();
|
|
730
|
+
ai.close();
|
|
731
|
+
|
|
732
|
+
// Reopen: the manifest-checkpointed counter continues.
|
|
733
|
+
const ai2 = Database.open(dirAi);
|
|
734
|
+
const t2 = ai2.getTable('things');
|
|
735
|
+
const r6 = t2.put([{ columnId: 2, bytes: Buffer.from('d') }]);
|
|
736
|
+
assert(r6.autoInc === 54n, `reopen continues counter (got ${r6.autoInc})`);
|
|
737
|
+
ai2.close();
|
|
738
|
+
|
|
739
|
+
// Seed-from-max: bulk-load explicit ids, reopen, first omitted allocation
|
|
740
|
+
// seeds to max(existing)+1 (no collision at 1).
|
|
741
|
+
const dirSeed = makeTempDir();
|
|
742
|
+
const seed = Database.withPath(dirSeed);
|
|
743
|
+
seed.createTable('legacy', {
|
|
744
|
+
columns: [
|
|
745
|
+
{ id: 1, name: 'id', ty: 1, primaryKey: true, nullable: false, autoIncrement: true },
|
|
746
|
+
{ id: 2, name: 'v', ty: 1, primaryKey: false, nullable: false },
|
|
747
|
+
],
|
|
748
|
+
indexes: [],
|
|
749
|
+
});
|
|
750
|
+
const sl = seed.getTable('legacy');
|
|
751
|
+
for (let i = 1; i <= 40; i++) {
|
|
752
|
+
sl.put([
|
|
753
|
+
{ columnId: 1, int64: BigInt(i) },
|
|
754
|
+
{ columnId: 2, int64: BigInt(i) },
|
|
755
|
+
]);
|
|
756
|
+
}
|
|
757
|
+
sl.commit();
|
|
758
|
+
sl.flush();
|
|
759
|
+
seed.close();
|
|
760
|
+
|
|
761
|
+
const seed2 = Database.open(dirSeed);
|
|
762
|
+
const sl2 = seed2.getTable('legacy');
|
|
763
|
+
const rs = sl2.put([{ columnId: 2, int64: 999n }]);
|
|
764
|
+
assert(rs.autoInc === 41n, `seed-from-max → 41 (got ${rs.autoInc})`);
|
|
765
|
+
seed2.close();
|
|
766
|
+
rmSync(dirAi, { recursive: true });
|
|
767
|
+
rmSync(dirSeed, { recursive: true });
|
|
768
|
+
}
|
|
769
|
+
console.log('smoke: AUTO_INCREMENT ✓');
|
|
770
|
+
|
|
771
|
+
// ── rename_table ──────────────────────────────────────────────────────────
|
|
772
|
+
{
|
|
773
|
+
const dir = mkdtempSync(join(tmpdir(), 'mc-rename-'));
|
|
774
|
+
const db = Database.withPath(dir);
|
|
775
|
+
db.createTable('a', schemaA);
|
|
776
|
+
db.createTable('b', schemaA);
|
|
777
|
+
|
|
778
|
+
// Basic rename: 'a' → 'c'.
|
|
779
|
+
db.renameTable('a', 'c');
|
|
780
|
+
const names = db.tableNames().sort();
|
|
781
|
+
assert(
|
|
782
|
+
JSON.stringify(names) === JSON.stringify(['b', 'c']),
|
|
783
|
+
`rename a→c: names=${JSON.stringify(names)}`
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
// Old name no longer resolves; new name does.
|
|
787
|
+
let oldGone = false;
|
|
788
|
+
try {
|
|
789
|
+
db.getTable('a');
|
|
790
|
+
} catch {
|
|
791
|
+
oldGone = true;
|
|
792
|
+
}
|
|
793
|
+
assert(oldGone, 'old name should not resolve after rename');
|
|
794
|
+
db.getTable('c'); // resolves without throwing
|
|
795
|
+
|
|
796
|
+
// Conflict: renaming onto an existing name must throw.
|
|
797
|
+
let conflict = false;
|
|
798
|
+
try {
|
|
799
|
+
db.renameTable('c', 'b');
|
|
800
|
+
} catch {
|
|
801
|
+
conflict = true;
|
|
802
|
+
}
|
|
803
|
+
assert(conflict, 'rename onto an existing name must fail');
|
|
804
|
+
|
|
805
|
+
// No-op rename (same name) succeeds.
|
|
806
|
+
db.renameTable('b', 'b');
|
|
807
|
+
|
|
808
|
+
// Empty new name is rejected.
|
|
809
|
+
let empty = false;
|
|
810
|
+
try {
|
|
811
|
+
db.renameTable('b', '');
|
|
812
|
+
} catch {
|
|
813
|
+
empty = true;
|
|
814
|
+
}
|
|
815
|
+
assert(empty, 'empty new name must be rejected');
|
|
816
|
+
|
|
817
|
+
// Durability: reopen keeps the new name.
|
|
818
|
+
db.close();
|
|
819
|
+
const db2 = Database.open(dir);
|
|
820
|
+
const reopened = db2.tableNames().sort();
|
|
821
|
+
assert(
|
|
822
|
+
JSON.stringify(reopened) === JSON.stringify(['b', 'c']),
|
|
823
|
+
`reopen after rename: ${JSON.stringify(reopened)}`
|
|
824
|
+
);
|
|
825
|
+
db2.close();
|
|
826
|
+
rmSync(dir, { recursive: true, force: true });
|
|
827
|
+
}
|
|
828
|
+
console.log('smoke: rename_table ✓');
|
|
829
|
+
|
|
830
|
+
console.log('All smoke tests passed.');
|