@delali/sirannon-db 0.1.6 → 0.1.7
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 +134 -19
- package/dist/backup-scheduler/index.d.ts +18 -3
- package/dist/backup-scheduler/index.mjs +2 -2
- package/dist/{change-tracker-CFTQ9TSn.d.ts → change-tracker-CbmaMO-N.d.ts} +12 -2
- package/dist/chunk-4IGMIJQK.mjs +318 -0
- package/dist/chunk-BNUTBHHH.mjs +22 -0
- package/dist/chunk-CJLYFDP5.mjs +26 -0
- package/dist/chunk-CW6S3WL5.mjs +222 -0
- package/dist/chunk-DJLX6CAE.mjs +20 -0
- package/dist/chunk-DVWQD3GF.mjs +49 -0
- package/dist/chunk-GEZUUIKV.mjs +268 -0
- package/dist/{chunk-UVMVN3OT.mjs → chunk-H5AB6NIR.mjs} +1 -1
- package/dist/{chunk-UTO3ZAFS.mjs → chunk-HHRMRFFR.mjs} +148 -28
- package/dist/chunk-TKGHYWQ6.mjs +35 -0
- package/dist/chunk-VLTICJOD.mjs +470 -0
- package/dist/{chunk-O7BHI3CF.mjs → chunk-YPYVQJ4C.mjs} +15 -1
- package/dist/client/index.d.ts +107 -15
- package/dist/client/index.mjs +348 -37
- package/dist/core/index.d.ts +70 -13
- package/dist/core/index.mjs +635 -192
- package/dist/core/writer-worker.d.ts +2 -0
- package/dist/core/writer-worker.mjs +107 -0
- package/dist/{database-BVY1GqE7.d.ts → database-DuGp0Rtr.d.ts} +51 -19
- package/dist/driver/better-sqlite3.d.ts +1 -1
- package/dist/driver/better-sqlite3.mjs +44 -6
- package/dist/driver/bun.mjs +35 -6
- package/dist/driver/expo.mjs +2 -2
- package/dist/driver/node.d.ts +1 -1
- package/dist/driver/node.mjs +48 -5
- package/dist/driver/wa-sqlite.d.ts +109 -0
- package/dist/driver/wa-sqlite.mjs +30 -2
- package/dist/{errors-C00ed08Q.d.ts → errors-5Nf5ZAEC.d.ts} +19 -1
- package/dist/file-migrations/index.d.ts +2 -3
- package/dist/file-migrations/index.mjs +1 -1
- package/dist/replication/coordinator/etcd.mjs +2 -2
- package/dist/replication/index.d.ts +7 -8
- package/dist/replication/index.mjs +79 -78
- package/dist/server/index.d.ts +111 -28
- package/dist/server/index.mjs +1016 -384
- package/dist/{sirannon-Cd-lK6T0.d.ts → sirannon-4SspRvP5.d.ts} +3 -3
- package/dist/transport/grpc.d.ts +3 -4
- package/dist/transport/grpc.mjs +2 -2
- package/dist/{types-Lc7ywFx7.d.ts → types-BsjobKbl.d.ts} +2 -2
- package/dist/{types-BeozgNPr.d.ts → types-D4p4UyDK.d.ts} +1 -1
- package/dist/types-D_hQW1hr.d.ts +494 -0
- package/package.json +3 -23
- package/dist/chunk-3MCMONVP.mjs +0 -115
- package/dist/chunk-74UN4DIE.mjs +0 -14
- package/dist/chunk-FB2U2Q3Y.mjs +0 -21
- package/dist/chunk-GS7T5YMI.mjs +0 -51
- package/dist/chunk-PXKAKK2V.mjs +0 -124
- package/dist/index-CLdNrcPz.d.ts +0 -16
- package/dist/types-BFSsG77t.d.ts +0 -29
- package/dist/types-D-74JiXb.d.ts +0 -265
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { ForbiddenSqlError } from './chunk-YPYVQJ4C.mjs';
|
|
2
|
+
|
|
3
|
+
// src/core/internal-tables.ts
|
|
4
|
+
var INTERNAL_TABLE_PREFIX = "_sirannon_";
|
|
5
|
+
var CHANGES_TABLE = "_sirannon_changes";
|
|
6
|
+
var META_TABLE = "_sirannon_meta";
|
|
7
|
+
var MIGRATIONS_TABLE = "_sirannon_migrations";
|
|
8
|
+
var PEER_STATE_TABLE = "_sirannon_peer_state";
|
|
9
|
+
var APPLIED_CHANGES_TABLE = "_sirannon_applied_changes";
|
|
10
|
+
var COLUMN_VERSIONS_TABLE = "_sirannon_column_versions";
|
|
11
|
+
var SYNC_STATE_TABLE = "_sirannon_sync_state";
|
|
12
|
+
var CDC_TRIGGER_PREFIX = "_sirannon_trg_";
|
|
13
|
+
var SIRANNON_PREFIX = "_sirannon";
|
|
14
|
+
var SQLITE_PREFIX = "sqlite_";
|
|
15
|
+
var SIRANNON_MESSAGE = "Access to internal tables is not permitted: identifiers beginning with '_sirannon' are reserved";
|
|
16
|
+
var SQLITE_WRITE_MESSAGE = "The SQLite schema catalogue is read-only: statements that modify a 'sqlite_' identifier are not permitted";
|
|
17
|
+
var ATTACH_MESSAGE = "ATTACH and DETACH statements are not permitted";
|
|
18
|
+
var WRITABLE_SCHEMA_MESSAGE = "PRAGMA writable_schema is not permitted";
|
|
19
|
+
var WRITE_VERBS = /* @__PURE__ */ new Set(["insert", "update", "delete", "replace", "create", "alter", "drop", "vacuum", "reindex"]);
|
|
20
|
+
function reservedKind(name) {
|
|
21
|
+
const lower = name.toLowerCase();
|
|
22
|
+
if (lower.startsWith(SIRANNON_PREFIX)) return "sirannon";
|
|
23
|
+
if (lower.startsWith(SQLITE_PREFIX)) return "sqlite";
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function isReservedIdentifier(name) {
|
|
27
|
+
return reservedKind(name) !== null;
|
|
28
|
+
}
|
|
29
|
+
function reservedSqlError(sql) {
|
|
30
|
+
const lower = sql.toLowerCase();
|
|
31
|
+
if (!lower.includes("_sirannon") && !lower.includes("sqlite_") && !lower.includes("attach") && !lower.includes("detach") && !lower.includes("writable_schema")) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return scan(sql);
|
|
35
|
+
}
|
|
36
|
+
function assertSqlAllowed(sql) {
|
|
37
|
+
const reason = reservedSqlError(sql);
|
|
38
|
+
if (reason) {
|
|
39
|
+
throw new ForbiddenSqlError(reason);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function isWordStart(c) {
|
|
43
|
+
return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_";
|
|
44
|
+
}
|
|
45
|
+
function isWordPart(c) {
|
|
46
|
+
return isWordStart(c) || c >= "0" && c <= "9" || c === "$";
|
|
47
|
+
}
|
|
48
|
+
function referenceError(kind, verb) {
|
|
49
|
+
if (kind === "sirannon") return SIRANNON_MESSAGE;
|
|
50
|
+
if (kind === "sqlite" && WRITE_VERBS.has(verb)) return SQLITE_WRITE_MESSAGE;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function scan(sql) {
|
|
54
|
+
const n = sql.length;
|
|
55
|
+
let i = 0;
|
|
56
|
+
let statementStart = true;
|
|
57
|
+
let verb = "";
|
|
58
|
+
while (i < n) {
|
|
59
|
+
const c = sql[i];
|
|
60
|
+
if (c === " " || c === " " || c === "\n" || c === "\r" || c === "\f" || c === "\v") {
|
|
61
|
+
i++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (c === "-" && sql[i + 1] === "-") {
|
|
65
|
+
i += 2;
|
|
66
|
+
while (i < n && sql[i] !== "\n") i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (c === "/" && sql[i + 1] === "*") {
|
|
70
|
+
i += 2;
|
|
71
|
+
while (i < n && !(sql[i] === "*" && sql[i + 1] === "/")) i++;
|
|
72
|
+
i += 2;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (c === "'") {
|
|
76
|
+
i = skipQuoted(sql, i, "'");
|
|
77
|
+
statementStart = false;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (c === '"' || c === "`") {
|
|
81
|
+
const delimited = readDelimited(sql, i, c);
|
|
82
|
+
const error = referenceError(reservedKind(delimited.value), verb);
|
|
83
|
+
if (error) return error;
|
|
84
|
+
i = delimited.next;
|
|
85
|
+
statementStart = false;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (c === "[") {
|
|
89
|
+
const end = sql.indexOf("]", i + 1);
|
|
90
|
+
const stop = end === -1 ? n : end;
|
|
91
|
+
const error = referenceError(reservedKind(sql.slice(i + 1, stop)), verb);
|
|
92
|
+
if (error) return error;
|
|
93
|
+
i = stop + 1;
|
|
94
|
+
statementStart = false;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (c === ";") {
|
|
98
|
+
i++;
|
|
99
|
+
statementStart = true;
|
|
100
|
+
verb = "";
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (isWordStart(c)) {
|
|
104
|
+
let j = i + 1;
|
|
105
|
+
while (j < n && isWordPart(sql[j])) j++;
|
|
106
|
+
const word = sql.slice(i, j);
|
|
107
|
+
const lowerWord = word.toLowerCase();
|
|
108
|
+
if (statementStart) {
|
|
109
|
+
verb = lowerWord;
|
|
110
|
+
if (lowerWord === "attach" || lowerWord === "detach") return ATTACH_MESSAGE;
|
|
111
|
+
} else if (verb === "pragma" && lowerWord === "writable_schema") {
|
|
112
|
+
return WRITABLE_SCHEMA_MESSAGE;
|
|
113
|
+
}
|
|
114
|
+
const error = referenceError(reservedKind(word), verb);
|
|
115
|
+
if (error) return error;
|
|
116
|
+
i = j;
|
|
117
|
+
statementStart = false;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
i++;
|
|
121
|
+
statementStart = false;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
function skipQuoted(sql, start, quote) {
|
|
126
|
+
const n = sql.length;
|
|
127
|
+
let i = start + 1;
|
|
128
|
+
while (i < n) {
|
|
129
|
+
if (sql[i] === quote) {
|
|
130
|
+
if (sql[i + 1] === quote) {
|
|
131
|
+
i += 2;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
return i + 1;
|
|
135
|
+
}
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
return n;
|
|
139
|
+
}
|
|
140
|
+
function readDelimited(sql, start, quote) {
|
|
141
|
+
const n = sql.length;
|
|
142
|
+
let i = start + 1;
|
|
143
|
+
let value = "";
|
|
144
|
+
while (i < n) {
|
|
145
|
+
if (sql[i] === quote) {
|
|
146
|
+
if (sql[i + 1] === quote) {
|
|
147
|
+
value += quote;
|
|
148
|
+
i += 2;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
return { value, next: i + 1 };
|
|
152
|
+
}
|
|
153
|
+
value += sql[i];
|
|
154
|
+
i++;
|
|
155
|
+
}
|
|
156
|
+
return { value, next: n };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/core/cdc/encoding.ts
|
|
160
|
+
var BLOB_TAG = "__sirannon_blob";
|
|
161
|
+
var INT_TAG = "__sirannon_int";
|
|
162
|
+
var SAFE_INT_BOUND_TEXT = "9007199254740991";
|
|
163
|
+
var SAFE_INT_BOUND = 9007199254740991n;
|
|
164
|
+
var HEX_BYTES_RE = /^[0-9a-fA-F]*$/;
|
|
165
|
+
var INT_PAYLOAD_RE = /^-?\d{1,19}$/;
|
|
166
|
+
function encodeTaggedValues(value) {
|
|
167
|
+
if (typeof value === "bigint" || isBinaryValue(value)) {
|
|
168
|
+
return encodeTaggedLeaf(value);
|
|
169
|
+
}
|
|
170
|
+
if (value === null || typeof value !== "object") return value;
|
|
171
|
+
if (Array.isArray(value)) {
|
|
172
|
+
return value.map(encodeTaggedValues);
|
|
173
|
+
}
|
|
174
|
+
if (typeof value.toJSON === "function") {
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
const obj = value;
|
|
178
|
+
const out = {};
|
|
179
|
+
for (const k of Object.keys(obj)) {
|
|
180
|
+
out[k] = encodeTaggedValues(obj[k]);
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
function encodeTaggedLeaf(value) {
|
|
185
|
+
if (typeof value === "bigint") {
|
|
186
|
+
if (value >= -SAFE_INT_BOUND && value <= SAFE_INT_BOUND) {
|
|
187
|
+
return Number(value);
|
|
188
|
+
}
|
|
189
|
+
return { [INT_TAG]: value.toString() };
|
|
190
|
+
}
|
|
191
|
+
if (isBinaryValue(value)) {
|
|
192
|
+
return { [BLOB_TAG]: encodeHexBytes(value) };
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
function encodeWireRowsInPlace(rows) {
|
|
197
|
+
for (let i = 0; i < rows.length; i++) {
|
|
198
|
+
const row = rows[i];
|
|
199
|
+
if (row === null || typeof row !== "object") continue;
|
|
200
|
+
const obj = row;
|
|
201
|
+
for (const k of Object.keys(obj)) {
|
|
202
|
+
const cell = obj[k];
|
|
203
|
+
if (typeof cell === "bigint" || isBinaryValue(cell)) {
|
|
204
|
+
obj[k] = encodeTaggedLeaf(cell);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return rows;
|
|
209
|
+
}
|
|
210
|
+
function decodeTaggedValues(value) {
|
|
211
|
+
if (value === null || typeof value !== "object") return value;
|
|
212
|
+
if (isBinaryValue(value)) return value;
|
|
213
|
+
if (Array.isArray(value)) {
|
|
214
|
+
return value.map(decodeTaggedValues);
|
|
215
|
+
}
|
|
216
|
+
const obj = value;
|
|
217
|
+
const keys = Object.keys(obj);
|
|
218
|
+
if (keys.length === 1) {
|
|
219
|
+
const tag = keys[0];
|
|
220
|
+
const tagValue = obj[tag];
|
|
221
|
+
if (tag === BLOB_TAG && typeof tagValue === "string") {
|
|
222
|
+
return decodeHexBytes(tagValue);
|
|
223
|
+
}
|
|
224
|
+
if (tag === INT_TAG && typeof tagValue === "string") {
|
|
225
|
+
if (!INT_PAYLOAD_RE.test(tagValue)) {
|
|
226
|
+
throw new Error("Invalid tagged integer payload");
|
|
227
|
+
}
|
|
228
|
+
return BigInt(tagValue);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const out = {};
|
|
232
|
+
for (const k of keys) {
|
|
233
|
+
out[k] = decodeTaggedValues(obj[k]);
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
function isBinaryValue(value) {
|
|
238
|
+
return value instanceof Uint8Array || typeof Buffer !== "undefined" && Buffer.isBuffer(value);
|
|
239
|
+
}
|
|
240
|
+
function encodeHexBytes(bytes) {
|
|
241
|
+
if (typeof Buffer !== "undefined") {
|
|
242
|
+
return Buffer.from(bytes).toString("hex").toUpperCase();
|
|
243
|
+
}
|
|
244
|
+
let hex = "";
|
|
245
|
+
for (const byte of bytes) {
|
|
246
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
247
|
+
}
|
|
248
|
+
return hex.toUpperCase();
|
|
249
|
+
}
|
|
250
|
+
function decodeHexBytes(hex) {
|
|
251
|
+
if (hex.length % 2 !== 0 || !HEX_BYTES_RE.test(hex)) {
|
|
252
|
+
throw new Error("Invalid CDC BLOB hex payload");
|
|
253
|
+
}
|
|
254
|
+
if (typeof Buffer !== "undefined") {
|
|
255
|
+
return Buffer.from(hex, "hex");
|
|
256
|
+
}
|
|
257
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
258
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
259
|
+
const byte = Number.parseInt(hex.slice(i, i + 2), 16);
|
|
260
|
+
if (Number.isNaN(byte)) {
|
|
261
|
+
throw new Error("Invalid CDC BLOB hex payload");
|
|
262
|
+
}
|
|
263
|
+
bytes[i / 2] = byte;
|
|
264
|
+
}
|
|
265
|
+
return bytes;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export { APPLIED_CHANGES_TABLE, CDC_TRIGGER_PREFIX, CHANGES_TABLE, COLUMN_VERSIONS_TABLE, INTERNAL_TABLE_PREFIX, META_TABLE, MIGRATIONS_TABLE, PEER_STATE_TABLE, SAFE_INT_BOUND_TEXT, SYNC_STATE_TABLE, assertSqlAllowed, decodeTaggedValues, encodeTaggedValues, encodeWireRowsInPlace, isReservedIdentifier };
|
|
@@ -1,11 +1,67 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { synchronousPragmaValue } from './chunk-CJLYFDP5.mjs';
|
|
2
|
+
import { CHANGES_TABLE, isReservedIdentifier, decodeTaggedValues, CDC_TRIGGER_PREFIX, SAFE_INT_BOUND_TEXT } from './chunk-GEZUUIKV.mjs';
|
|
3
|
+
import { SirannonError, ForbiddenSqlError, CDCError } from './chunk-YPYVQJ4C.mjs';
|
|
4
|
+
|
|
5
|
+
// src/core/bulk-load.ts
|
|
6
|
+
var DEFAULT_LOAD_DURABILITY = "off";
|
|
7
|
+
var CHECKPOINT_ATTEMPTS = 3;
|
|
8
|
+
var CHECKPOINT_RETRY_DELAY_MS = 50;
|
|
9
|
+
function isBulkLoadDurability(value) {
|
|
10
|
+
return value === "off" || value === "normal";
|
|
11
|
+
}
|
|
12
|
+
async function runBulkLoad(run) {
|
|
13
|
+
const durability = run.durability ?? DEFAULT_LOAD_DURABILITY;
|
|
14
|
+
if (!isBulkLoadDurability(durability)) {
|
|
15
|
+
throw new SirannonError("Bulk load durability must be 'off' or 'normal'", "INVALID_DURABILITY");
|
|
16
|
+
}
|
|
17
|
+
const restorePragma = `PRAGMA synchronous = ${synchronousPragmaValue(run.configuredSynchronous)}`;
|
|
18
|
+
await run.writer.exec(`PRAGMA synchronous = ${synchronousPragmaValue(durability)}`);
|
|
19
|
+
let result;
|
|
20
|
+
let loadError;
|
|
21
|
+
try {
|
|
22
|
+
result = await run.loadRows();
|
|
23
|
+
} catch (err) {
|
|
24
|
+
loadError = err;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
await run.writer.exec(restorePragma);
|
|
28
|
+
} catch {
|
|
29
|
+
if (loadError !== void 0) throw loadError;
|
|
30
|
+
throw new SirannonError(
|
|
31
|
+
"Bulk load committed but durability could not be restored because the writer connection failed",
|
|
32
|
+
"DURABILITY_RESTORE_FAILED"
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (loadError !== void 0) {
|
|
36
|
+
throw loadError;
|
|
37
|
+
}
|
|
38
|
+
if (result === void 0) {
|
|
39
|
+
throw new SirannonError("Bulk load completed without a result", "INTERNAL_ERROR");
|
|
40
|
+
}
|
|
41
|
+
if ((run.checkpoint ?? true) && run.walMode && result.changes > 0) {
|
|
42
|
+
await checkpoint(run.writer);
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
async function checkpoint(writer) {
|
|
47
|
+
for (let attempt = 0; attempt < CHECKPOINT_ATTEMPTS; attempt++) {
|
|
48
|
+
if (attempt > 0) {
|
|
49
|
+
await delay(attempt * CHECKPOINT_RETRY_DELAY_MS);
|
|
50
|
+
}
|
|
51
|
+
const stmt = await writer.prepare("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
52
|
+
const row = await stmt.get();
|
|
53
|
+
if (!row || row.busy === 0) return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function delay(ms) {
|
|
57
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
58
|
+
}
|
|
3
59
|
|
|
4
60
|
// src/core/cdc/trigger-sql.ts
|
|
5
61
|
async function dropCdcTriggers(conn, table) {
|
|
6
|
-
await conn.exec(`DROP TRIGGER IF EXISTS "
|
|
7
|
-
await conn.exec(`DROP TRIGGER IF EXISTS "
|
|
8
|
-
await conn.exec(`DROP TRIGGER IF EXISTS "
|
|
62
|
+
await conn.exec(`DROP TRIGGER IF EXISTS "${CDC_TRIGGER_PREFIX}${table}_insert"`);
|
|
63
|
+
await conn.exec(`DROP TRIGGER IF EXISTS "${CDC_TRIGGER_PREFIX}${table}_update"`);
|
|
64
|
+
await conn.exec(`DROP TRIGGER IF EXISTS "${CDC_TRIGGER_PREFIX}${table}_delete"`);
|
|
9
65
|
}
|
|
10
66
|
async function installCdcTriggers(conn, changesTable, table, columns, pkColumns, replication) {
|
|
11
67
|
const newJson = buildJsonObject(columns, "NEW");
|
|
@@ -15,7 +71,7 @@ async function installCdcTriggers(conn, changesTable, table, columns, pkColumns,
|
|
|
15
71
|
const replCols = replication ? ", node_id, tx_id, hlc" : "";
|
|
16
72
|
const replVals = replication ? ", '', '', ''" : "";
|
|
17
73
|
await conn.exec(`
|
|
18
|
-
CREATE TRIGGER IF NOT EXISTS "
|
|
74
|
+
CREATE TRIGGER IF NOT EXISTS "${CDC_TRIGGER_PREFIX}${table}_insert"
|
|
19
75
|
AFTER INSERT ON "${table}"
|
|
20
76
|
BEGIN
|
|
21
77
|
INSERT INTO "${changesTable}" (table_name, operation, row_id, new_data${replCols})
|
|
@@ -23,7 +79,7 @@ async function installCdcTriggers(conn, changesTable, table, columns, pkColumns,
|
|
|
23
79
|
END
|
|
24
80
|
`);
|
|
25
81
|
await conn.exec(`
|
|
26
|
-
CREATE TRIGGER IF NOT EXISTS "
|
|
82
|
+
CREATE TRIGGER IF NOT EXISTS "${CDC_TRIGGER_PREFIX}${table}_update"
|
|
27
83
|
AFTER UPDATE ON "${table}"
|
|
28
84
|
BEGIN
|
|
29
85
|
INSERT INTO "${changesTable}" (table_name, operation, row_id, old_data, new_data${replCols})
|
|
@@ -31,7 +87,7 @@ async function installCdcTriggers(conn, changesTable, table, columns, pkColumns,
|
|
|
31
87
|
END
|
|
32
88
|
`);
|
|
33
89
|
await conn.exec(`
|
|
34
|
-
CREATE TRIGGER IF NOT EXISTS "
|
|
90
|
+
CREATE TRIGGER IF NOT EXISTS "${CDC_TRIGGER_PREFIX}${table}_delete"
|
|
35
91
|
AFTER DELETE ON "${table}"
|
|
36
92
|
BEGIN
|
|
37
93
|
INSERT INTO "${changesTable}" (table_name, operation, row_id, old_data${replCols})
|
|
@@ -64,7 +120,6 @@ function escStr(name) {
|
|
|
64
120
|
|
|
65
121
|
// src/core/cdc/change-tracker.ts
|
|
66
122
|
var DEFAULT_RETENTION_MS = 36e5;
|
|
67
|
-
var DEFAULT_CHANGES_TABLE = "_sirannon_changes";
|
|
68
123
|
var DEFAULT_POLL_BATCH_SIZE = 1e3;
|
|
69
124
|
var IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
70
125
|
var ChangeTracker = class {
|
|
@@ -80,13 +135,16 @@ var ChangeTracker = class {
|
|
|
80
135
|
pruneBoundary = null;
|
|
81
136
|
constructor(options) {
|
|
82
137
|
this.retentionMs = options?.retention ?? DEFAULT_RETENTION_MS;
|
|
83
|
-
this.changesTable = options?.changesTable ??
|
|
138
|
+
this.changesTable = options?.changesTable ?? CHANGES_TABLE;
|
|
84
139
|
this.pollBatchSize = options?.pollBatchSize ?? DEFAULT_POLL_BATCH_SIZE;
|
|
85
140
|
this.replication = options?.replication ?? false;
|
|
86
141
|
this.assertIdentifier(this.changesTable, "changes table name");
|
|
87
142
|
}
|
|
88
143
|
async watch(conn, table) {
|
|
89
144
|
this.assertIdentifier(table, "table name");
|
|
145
|
+
if (isReservedIdentifier(table)) {
|
|
146
|
+
throw new ForbiddenSqlError(`Table '${table}' is reserved for Sirannon and cannot be watched`);
|
|
147
|
+
}
|
|
90
148
|
await this.ensureChangesTable(conn);
|
|
91
149
|
const columns = await this.getColumns(conn, table);
|
|
92
150
|
if (columns.length === 0) {
|
|
@@ -226,20 +284,64 @@ var ChangeTracker = class {
|
|
|
226
284
|
if (rows.length === 0) {
|
|
227
285
|
return [];
|
|
228
286
|
}
|
|
229
|
-
const events =
|
|
230
|
-
for (const row of rows) {
|
|
231
|
-
events.push({
|
|
232
|
-
type: row.operation.toLowerCase(),
|
|
233
|
-
table: row.table_name,
|
|
234
|
-
row: row.new_data ? decodeTaggedValues(JSON.parse(row.new_data)) : {},
|
|
235
|
-
oldRow: row.old_data ? decodeTaggedValues(JSON.parse(row.old_data)) : void 0,
|
|
236
|
-
seq: BigInt(row.seq),
|
|
237
|
-
timestamp: row.changed_at
|
|
238
|
-
});
|
|
239
|
-
}
|
|
287
|
+
const events = rows.map((row) => this.rowToEvent(row));
|
|
240
288
|
this.lastSeq = BigInt(rows[rows.length - 1].seq);
|
|
241
289
|
return events;
|
|
242
290
|
}
|
|
291
|
+
/** The highest seq already polled; live subscribers receive events beyond it. */
|
|
292
|
+
get cursor() {
|
|
293
|
+
return this.lastSeq;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Reads retained changes for one table with seq in `(afterSeq, upToSeq]`,
|
|
297
|
+
* ordered ascending and capped at `limit`. Used to replay history to a
|
|
298
|
+
* resuming subscriber without disturbing the shared poll cursor.
|
|
299
|
+
*/
|
|
300
|
+
async readSince(conn, table, afterSeq, upToSeq, limit) {
|
|
301
|
+
if (!this.changesTableReady) {
|
|
302
|
+
await this.detectChangesTable(conn);
|
|
303
|
+
if (!this.changesTableReady) {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const stmt = await this.getStmt(
|
|
308
|
+
conn,
|
|
309
|
+
"read_since",
|
|
310
|
+
`SELECT seq, table_name, operation, row_id, changed_at, old_data, new_data
|
|
311
|
+
FROM "${this.changesTable}"
|
|
312
|
+
WHERE table_name = ? AND seq > ? AND seq <= ?
|
|
313
|
+
ORDER BY seq ASC
|
|
314
|
+
LIMIT ?`
|
|
315
|
+
);
|
|
316
|
+
const rows = await stmt.all(table, afterSeq.toString(), upToSeq.toString(), limit);
|
|
317
|
+
return rows.map((row) => this.rowToEvent(row));
|
|
318
|
+
}
|
|
319
|
+
/** The lowest retained seq, or `null` when the change log is empty. */
|
|
320
|
+
async getMinSeq(conn) {
|
|
321
|
+
if (!this.changesTableReady) {
|
|
322
|
+
await this.detectChangesTable(conn);
|
|
323
|
+
if (!this.changesTableReady) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const stmt = await this.getStmt(conn, "min_seq", `SELECT MIN(seq) AS seq FROM "${this.changesTable}"`);
|
|
328
|
+
const row = await stmt.get();
|
|
329
|
+
const seq = row?.seq;
|
|
330
|
+
if (seq === void 0 || seq === null) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
return typeof seq === "bigint" ? seq : BigInt(String(seq));
|
|
334
|
+
}
|
|
335
|
+
rowToEvent(row) {
|
|
336
|
+
return {
|
|
337
|
+
type: row.operation.toLowerCase(),
|
|
338
|
+
table: row.table_name,
|
|
339
|
+
row: row.new_data ? decodeTaggedValues(JSON.parse(row.new_data)) : {},
|
|
340
|
+
oldRow: row.old_data ? decodeTaggedValues(JSON.parse(row.old_data)) : void 0,
|
|
341
|
+
seq: BigInt(row.seq),
|
|
342
|
+
timestamp: row.changed_at
|
|
343
|
+
};
|
|
344
|
+
}
|
|
243
345
|
async advanceToLatest(conn) {
|
|
244
346
|
if (!this.changesTableReady) {
|
|
245
347
|
await this.detectChangesTable(conn);
|
|
@@ -430,7 +532,7 @@ var SubscriptionManager = class {
|
|
|
430
532
|
for (const id of ids) {
|
|
431
533
|
const sub = this.subscriptions.get(id);
|
|
432
534
|
if (!sub) continue;
|
|
433
|
-
if (sub.filter && !
|
|
535
|
+
if (sub.filter && !changeMatchesFilter(event, sub.filter)) {
|
|
434
536
|
continue;
|
|
435
537
|
}
|
|
436
538
|
try {
|
|
@@ -461,18 +563,19 @@ var SubscriptionBuilderImpl = class {
|
|
|
461
563
|
return this.manager.subscribe(this.table, this.conditions, callback);
|
|
462
564
|
}
|
|
463
565
|
};
|
|
464
|
-
function startPolling(conn, tracker, manager, intervalMs, onError) {
|
|
566
|
+
function startPolling(conn, tracker, manager, intervalMs, onError, runExclusive) {
|
|
465
567
|
let consecutiveErrors = 0;
|
|
466
568
|
let tickCount = 0;
|
|
467
569
|
let polling = false;
|
|
468
570
|
const MAX_CONSECUTIVE_ERRORS = 10;
|
|
469
571
|
const CLEANUP_INTERVAL_TICKS = 100;
|
|
572
|
+
const exclusive = runExclusive ?? ((operation) => operation());
|
|
470
573
|
const tick = async () => {
|
|
471
574
|
if (manager.size === 0) return;
|
|
472
575
|
if (polling) return;
|
|
473
576
|
polling = true;
|
|
474
577
|
try {
|
|
475
|
-
const events = await tracker.poll(conn);
|
|
578
|
+
const events = await exclusive(() => tracker.poll(conn));
|
|
476
579
|
if (events.length > 0) {
|
|
477
580
|
manager.dispatch(events);
|
|
478
581
|
}
|
|
@@ -480,7 +583,7 @@ function startPolling(conn, tracker, manager, intervalMs, onError) {
|
|
|
480
583
|
tickCount++;
|
|
481
584
|
if (tickCount >= CLEANUP_INTERVAL_TICKS) {
|
|
482
585
|
tickCount = 0;
|
|
483
|
-
await tracker.cleanup(conn);
|
|
586
|
+
await exclusive(() => tracker.cleanup(conn));
|
|
484
587
|
}
|
|
485
588
|
} catch (err) {
|
|
486
589
|
consecutiveErrors++;
|
|
@@ -501,14 +604,31 @@ function startPolling(conn, tracker, manager, intervalMs, onError) {
|
|
|
501
604
|
};
|
|
502
605
|
return stop;
|
|
503
606
|
}
|
|
504
|
-
function
|
|
607
|
+
function changeMatchesFilter(event, filter) {
|
|
505
608
|
const target = event.type === "delete" ? event.oldRow ?? {} : event.row;
|
|
506
609
|
for (const [key, value] of Object.entries(filter)) {
|
|
507
|
-
if (target[key]
|
|
610
|
+
if (!filterValueMatches(target[key], value)) {
|
|
508
611
|
return false;
|
|
509
612
|
}
|
|
510
613
|
}
|
|
511
614
|
return true;
|
|
512
615
|
}
|
|
616
|
+
function filterValueMatches(rowValue, filterValue) {
|
|
617
|
+
if (rowValue === filterValue) return true;
|
|
618
|
+
if (typeof rowValue === "bigint" && typeof filterValue === "number") {
|
|
619
|
+
return Number.isInteger(filterValue) && BigInt(filterValue) === rowValue;
|
|
620
|
+
}
|
|
621
|
+
if (typeof rowValue === "number" && typeof filterValue === "bigint") {
|
|
622
|
+
return Number.isInteger(rowValue) && BigInt(rowValue) === filterValue;
|
|
623
|
+
}
|
|
624
|
+
if (rowValue instanceof Uint8Array && filterValue instanceof Uint8Array) {
|
|
625
|
+
if (rowValue.byteLength !== filterValue.byteLength) return false;
|
|
626
|
+
for (let i = 0; i < rowValue.byteLength; i++) {
|
|
627
|
+
if (rowValue[i] !== filterValue[i]) return false;
|
|
628
|
+
}
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
513
633
|
|
|
514
|
-
export { ChangeTracker, SubscriptionBuilderImpl, SubscriptionManager, startPolling };
|
|
634
|
+
export { ChangeTracker, SubscriptionBuilderImpl, SubscriptionManager, changeMatchesFilter, isBulkLoadDurability, runBulkLoad, startPolling };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { query, execute, executeBatch } from './chunk-CW6S3WL5.mjs';
|
|
2
|
+
|
|
3
|
+
// src/core/transaction.ts
|
|
4
|
+
var Transaction = class _Transaction {
|
|
5
|
+
constructor(conn) {
|
|
6
|
+
this.conn = conn;
|
|
7
|
+
}
|
|
8
|
+
_lastInsertRowId = 0;
|
|
9
|
+
async query(sql, params) {
|
|
10
|
+
return query(this.conn, sql, params);
|
|
11
|
+
}
|
|
12
|
+
async execute(sql, params) {
|
|
13
|
+
const result = await execute(this.conn, sql, params);
|
|
14
|
+
this._lastInsertRowId = result.lastInsertRowId;
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
async executeBatch(sql, paramsBatch) {
|
|
18
|
+
const results = await executeBatch(this.conn, sql, paramsBatch);
|
|
19
|
+
if (results.length > 0) {
|
|
20
|
+
this._lastInsertRowId = results[results.length - 1].lastInsertRowId;
|
|
21
|
+
}
|
|
22
|
+
return results;
|
|
23
|
+
}
|
|
24
|
+
get lastInsertRowId() {
|
|
25
|
+
return this._lastInsertRowId;
|
|
26
|
+
}
|
|
27
|
+
static async run(conn, fn) {
|
|
28
|
+
return conn.transaction(async (txConn) => {
|
|
29
|
+
const tx = new _Transaction(txConn);
|
|
30
|
+
return fn(tx);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export { Transaction };
|