orez 0.2.25 → 0.2.27
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/dist/cf-do/watermark.d.ts +21 -0
- package/dist/cf-do/watermark.d.ts.map +1 -0
- package/dist/cf-do/watermark.js +93 -0
- package/dist/cf-do/watermark.js.map +1 -0
- package/dist/cf-do/worker.d.ts +48 -22
- package/dist/cf-do/worker.d.ts.map +1 -1
- package/dist/cf-do/worker.js +650 -269
- package/dist/cf-do/worker.js.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/do-sql-tracking.d.ts +6 -0
- package/dist/do-sql-tracking.d.ts.map +1 -0
- package/dist/do-sql-tracking.js +14 -0
- package/dist/do-sql-tracking.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -14
- package/dist/index.js.map +1 -1
- package/dist/pg-proxy-browser.js +6 -6
- package/dist/pg-proxy-browser.js.map +1 -1
- package/dist/pg-proxy-do-backend.d.ts +98 -17
- package/dist/pg-proxy-do-backend.d.ts.map +1 -1
- package/dist/pg-proxy-do-backend.js +6075 -454
- package/dist/pg-proxy-do-backend.js.map +1 -1
- package/dist/pg-sqlite-compiler/catalog/seed.d.ts +67 -0
- package/dist/pg-sqlite-compiler/catalog/seed.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/catalog/seed.js +436 -0
- package/dist/pg-sqlite-compiler/catalog/seed.js.map +1 -0
- package/dist/pg-sqlite-compiler/index.d.ts +12 -0
- package/dist/pg-sqlite-compiler/index.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/index.js +59 -0
- package/dist/pg-sqlite-compiler/index.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts +48 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.js +93 -0
- package/dist/pg-sqlite-compiler/passes/ast-utils.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/catalog.d.ts +34 -0
- package/dist/pg-sqlite-compiler/passes/catalog.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/catalog.js +30 -0
- package/dist/pg-sqlite-compiler/passes/catalog.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/datetime.d.ts +21 -0
- package/dist/pg-sqlite-compiler/passes/datetime.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/datetime.js +53 -0
- package/dist/pg-sqlite-compiler/passes/datetime.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/index.d.ts +21 -0
- package/dist/pg-sqlite-compiler/passes/index.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/index.js +39 -0
- package/dist/pg-sqlite-compiler/passes/index.js.map +1 -0
- package/dist/pg-sqlite-compiler/passes/types.d.ts +41 -0
- package/dist/pg-sqlite-compiler/passes/types.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/passes/types.js +103 -0
- package/dist/pg-sqlite-compiler/passes/types.js.map +1 -0
- package/dist/pg-sqlite-compiler/test/oracle.d.ts +34 -0
- package/dist/pg-sqlite-compiler/test/oracle.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/test/oracle.js +204 -0
- package/dist/pg-sqlite-compiler/test/oracle.js.map +1 -0
- package/dist/pg-sqlite-compiler/types.d.ts +55 -0
- package/dist/pg-sqlite-compiler/types.d.ts.map +1 -0
- package/dist/pg-sqlite-compiler/types.js +2 -0
- package/dist/pg-sqlite-compiler/types.js.map +1 -0
- package/dist/replication/change-tracker.d.ts.map +1 -1
- package/dist/replication/change-tracker.js +18 -1
- package/dist/replication/change-tracker.js.map +1 -1
- package/dist/replication/handler.d.ts.map +1 -1
- package/dist/replication/handler.js +7 -2
- package/dist/replication/handler.js.map +1 -1
- package/dist/replication/pgoutput-encoder.d.ts.map +1 -1
- package/dist/replication/pgoutput-encoder.js +72 -30
- package/dist/replication/pgoutput-encoder.js.map +1 -1
- package/dist/worker/browser-build-config.d.ts.map +1 -1
- package/dist/worker/browser-build-config.js +2 -1
- package/dist/worker/browser-build-config.js.map +1 -1
- package/dist/worker/cf-patches.d.ts +5 -2
- package/dist/worker/cf-patches.d.ts.map +1 -1
- package/dist/worker/cf-patches.js +238 -4
- package/dist/worker/cf-patches.js.map +1 -1
- package/dist/worker/shims/node-stub.d.ts +35 -0
- package/dist/worker/shims/node-stub.d.ts.map +1 -1
- package/dist/worker/shims/node-stub.js +53 -1
- package/dist/worker/shims/node-stub.js.map +1 -1
- package/dist/worker/shims/oxfmt.d.ts +4 -0
- package/dist/worker/shims/oxfmt.d.ts.map +1 -0
- package/dist/worker/shims/oxfmt.js +4 -0
- package/dist/worker/shims/oxfmt.js.map +1 -0
- package/dist/worker/shims/postgres-socket.js +1 -1
- package/dist/worker/shims/postgres-socket.js.map +1 -1
- package/dist/worker/shims/sqlite.d.ts +1 -0
- package/dist/worker/shims/sqlite.d.ts.map +1 -1
- package/dist/worker/shims/sqlite.js +229 -9
- package/dist/worker/shims/sqlite.js.map +1 -1
- package/dist/worker/shims/ws.d.ts.map +1 -1
- package/dist/worker/shims/ws.js +45 -0
- package/dist/worker/shims/ws.js.map +1 -1
- package/dist/worker/shims/zero-process-env.d.ts +2 -0
- package/dist/worker/shims/zero-process-env.d.ts.map +1 -0
- package/dist/worker/shims/zero-process-env.js +9 -0
- package/dist/worker/shims/zero-process-env.js.map +1 -0
- package/dist/worker/zero-cache-embed-cf.d.ts +29 -12
- package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -1
- package/dist/worker/zero-cache-embed-cf.js +83 -14
- package/dist/worker/zero-cache-embed-cf.js.map +1 -1
- package/package.json +11 -2
- package/src/cf-do/.wrangler/cache/cf.json +1 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/cache/miniflare-CacheObject/metadata.sqlite-wal +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/0ffaabee41a60e04dd0eb7db3073f0a40139e6a97ccd26823967acb652b89a7b.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-shm +0 -0
- package/src/cf-do/.wrangler/state/v3/do/zero-do-ZeroDO/metadata.sqlite-wal +0 -0
- package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-insertion-facade.js +11 -0
- package/src/cf-do/.wrangler/tmp/bundle-0z4CpE/middleware-loader.entry.ts +134 -0
- package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-insertion-facade.js +11 -0
- package/src/cf-do/.wrangler/tmp/bundle-vYmw0E/middleware-loader.entry.ts +134 -0
- package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js +1059 -0
- package/src/cf-do/.wrangler/tmp/dev-cbILNo/worker.js.map +8 -0
- package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js +1059 -0
- package/src/cf-do/.wrangler/tmp/dev-qbho19/worker.js.map +8 -0
- package/src/cf-do/ARCHITECTURE.md +93 -0
- package/src/cf-do/CHAT_E2E.md +213 -0
- package/src/cf-do/watermark.test.ts +103 -0
- package/src/cf-do/watermark.ts +118 -0
- package/src/cf-do/worker.ts +1041 -0
- package/src/cf-do/wrangler.toml +11 -0
- package/src/cli.test.ts +3 -1
- package/src/config.ts +1 -1
- package/src/do-sql-tracking.test.ts +19 -0
- package/src/do-sql-tracking.ts +19 -0
- package/src/index.ts +29 -14
- package/src/pg-proxy-browser.ts +6 -6
- package/src/pg-proxy-do-backend.test.ts +3890 -0
- package/src/pg-proxy-do-backend.ts +6833 -482
- package/src/pg-sqlite-compiler/README.md +53 -0
- package/src/pg-sqlite-compiler/catalog/seed.ts +524 -0
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/arithmetic.json +307 -0
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/array.json +377 -0
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/cast.json +12 -0
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/catalog.json +447 -0
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/create-table.json +32 -0
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/datetime.json +397 -0
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/enum.json +337 -0
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/insert.json +337 -0
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/json.json +537 -0
- package/src/pg-sqlite-compiler/fixtures/pgsqlite/misc.json +1837 -0
- package/src/pg-sqlite-compiler/index.ts +73 -0
- package/src/pg-sqlite-compiler/integration.test.ts +136 -0
- package/src/pg-sqlite-compiler/passes/ast-utils.ts +113 -0
- package/src/pg-sqlite-compiler/passes/catalog.ts +65 -0
- package/src/pg-sqlite-compiler/passes/datetime.ts +74 -0
- package/src/pg-sqlite-compiler/passes/index.ts +49 -0
- package/src/pg-sqlite-compiler/passes/types.ts +156 -0
- package/src/pg-sqlite-compiler/smoke.test.ts +69 -0
- package/src/pg-sqlite-compiler/test/catalog.test.ts +171 -0
- package/src/pg-sqlite-compiler/test/corpus.test.ts +161 -0
- package/src/pg-sqlite-compiler/test/datetime.oracle.test.ts +102 -0
- package/src/pg-sqlite-compiler/test/oracle.ts +237 -0
- package/src/pg-sqlite-compiler/test/types.test.ts +109 -0
- package/src/pg-sqlite-compiler/types.ts +63 -0
- package/src/replication/change-tracker.ts +16 -1
- package/src/replication/handler.test.ts +35 -0
- package/src/replication/handler.ts +7 -2
- package/src/replication/pgoutput-encoder.test.ts +71 -2
- package/src/replication/pgoutput-encoder.ts +65 -30
- package/src/worker/browser-build-config.test.ts +12 -0
- package/src/worker/browser-build-config.ts +2 -1
- package/src/worker/cf-patches.ts +274 -4
- package/src/worker/shims/node-stub.ts +53 -1
- package/src/worker/shims/oxfmt.ts +3 -0
- package/src/worker/shims/postgres-socket.ts +1 -1
- package/src/worker/shims/sqlite.test.ts +145 -0
- package/src/worker/shims/sqlite.ts +256 -9
- package/src/worker/shims/ws.ts +45 -0
- package/src/worker/shims/zero-process-env.ts +11 -0
- package/src/worker/zero-cache-embed-cf.ts +114 -18
- package/src/query-rewrites.test.ts +0 -30
- package/src/query-rewrites.ts +0 -152
|
@@ -139,6 +139,7 @@ describe('Database', () => {
|
|
|
139
139
|
})
|
|
140
140
|
|
|
141
141
|
afterEach(() => {
|
|
142
|
+
db.close()
|
|
142
143
|
mock._nativeDb.close()
|
|
143
144
|
})
|
|
144
145
|
|
|
@@ -189,6 +190,7 @@ describe('Database.exec', () => {
|
|
|
189
190
|
})
|
|
190
191
|
|
|
191
192
|
afterEach(() => {
|
|
193
|
+
db.close()
|
|
192
194
|
mock._nativeDb.close()
|
|
193
195
|
})
|
|
194
196
|
|
|
@@ -229,6 +231,7 @@ describe('Database.prepare / Statement', () => {
|
|
|
229
231
|
})
|
|
230
232
|
|
|
231
233
|
afterEach(() => {
|
|
234
|
+
db.close()
|
|
232
235
|
mock._nativeDb.close()
|
|
233
236
|
})
|
|
234
237
|
|
|
@@ -552,6 +555,148 @@ describe('StatementRunner', () => {
|
|
|
552
555
|
})
|
|
553
556
|
})
|
|
554
557
|
|
|
558
|
+
describe('DO snapshot transactions', () => {
|
|
559
|
+
let mock: SqlStorageLike & { _nativeDb: any; transactionSync: <T>(fn: () => T) => T }
|
|
560
|
+
let live: Database
|
|
561
|
+
let snapshot: Database
|
|
562
|
+
|
|
563
|
+
beforeEach(() => {
|
|
564
|
+
mock = createMockSqlStorage() as SqlStorageLike & {
|
|
565
|
+
_nativeDb: any
|
|
566
|
+
transactionSync: <T>(fn: () => T) => T
|
|
567
|
+
}
|
|
568
|
+
mock.transactionSync = (fn) => fn()
|
|
569
|
+
live = new Database(mock)
|
|
570
|
+
snapshot = new Database(mock)
|
|
571
|
+
live.exec('CREATE TABLE todo (id TEXT PRIMARY KEY, title TEXT, _0_version TEXT)')
|
|
572
|
+
live.prepare('INSERT INTO todo VALUES (?, ?, ?)').run('1', 'old', '01')
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
afterEach(() => {
|
|
576
|
+
live.close()
|
|
577
|
+
snapshot.close()
|
|
578
|
+
mock._nativeDb.close()
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it('keeps BEGIN CONCURRENT reads stable until rollback', () => {
|
|
582
|
+
snapshot.prepare('BEGIN CONCURRENT').run()
|
|
583
|
+
expect(snapshot.prepare('SELECT title FROM todo WHERE id = ?').get('1')).toEqual({
|
|
584
|
+
title: 'old',
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
live
|
|
588
|
+
.prepare('UPDATE todo SET title = ?, _0_version = ? WHERE id = ?')
|
|
589
|
+
.run('new', '02', '1')
|
|
590
|
+
|
|
591
|
+
expect(live.prepare('SELECT title FROM todo WHERE id = ?').get('1')).toEqual({
|
|
592
|
+
title: 'new',
|
|
593
|
+
})
|
|
594
|
+
expect(snapshot.prepare('SELECT title FROM todo WHERE id = ?').get('1')).toEqual({
|
|
595
|
+
title: 'old',
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
snapshot.prepare('ROLLBACK').run()
|
|
599
|
+
expect(snapshot.prepare('SELECT title FROM todo WHERE id = ?').get('1')).toEqual({
|
|
600
|
+
title: 'new',
|
|
601
|
+
})
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it('hides snapshot tables from sqlite catalog queries', () => {
|
|
605
|
+
snapshot.prepare('BEGIN CONCURRENT').run()
|
|
606
|
+
|
|
607
|
+
expect(
|
|
608
|
+
live
|
|
609
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name")
|
|
610
|
+
.all()
|
|
611
|
+
).toEqual([{ name: 'todo' }])
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
it('does not rewrite table names inside string literals during snapshots', () => {
|
|
615
|
+
live.exec('CREATE TABLE log (id TEXT PRIMARY KEY, table_name TEXT, _0_version TEXT)')
|
|
616
|
+
live.prepare('INSERT INTO log VALUES (?, ?, ?)').run('1', 'todo', '01')
|
|
617
|
+
live.prepare('INSERT INTO log VALUES (?, ?, ?)').run('2', '"todo"', '02')
|
|
618
|
+
|
|
619
|
+
snapshot.prepare('BEGIN CONCURRENT').run()
|
|
620
|
+
|
|
621
|
+
expect(
|
|
622
|
+
snapshot.prepare("SELECT table_name FROM log WHERE table_name = 'todo'").get()
|
|
623
|
+
).toEqual({ table_name: 'todo' })
|
|
624
|
+
expect(
|
|
625
|
+
snapshot.prepare('SELECT table_name FROM log WHERE table_name = ?').get('"todo"')
|
|
626
|
+
).toEqual({ table_name: '"todo"' })
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
it('cleans inactive snapshot tables when opening a database', () => {
|
|
630
|
+
mock.exec('CREATE TABLE _orez_snapshot_1_todo AS SELECT * FROM todo')
|
|
631
|
+
expect(
|
|
632
|
+
mock
|
|
633
|
+
.exec("SELECT name FROM sqlite_master WHERE name = '_orez_snapshot_1_todo'")
|
|
634
|
+
.toArray()
|
|
635
|
+
).toEqual([{ name: '_orez_snapshot_1_todo' }])
|
|
636
|
+
|
|
637
|
+
const reopened = new Database(mock)
|
|
638
|
+
try {
|
|
639
|
+
expect(
|
|
640
|
+
mock
|
|
641
|
+
.exec("SELECT name FROM sqlite_master WHERE name = '_orez_snapshot_1_todo'")
|
|
642
|
+
.toArray()
|
|
643
|
+
).toEqual([])
|
|
644
|
+
} finally {
|
|
645
|
+
reopened.close()
|
|
646
|
+
}
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
it('does not remove another open connection snapshot', () => {
|
|
650
|
+
snapshot.prepare('BEGIN CONCURRENT').run()
|
|
651
|
+
const snapshotTables = mock
|
|
652
|
+
.exec("SELECT name FROM sqlite_master WHERE name LIKE '_orez_snapshot_%'")
|
|
653
|
+
.toArray()
|
|
654
|
+
expect(snapshotTables).toHaveLength(1)
|
|
655
|
+
|
|
656
|
+
const other = new Database(mock)
|
|
657
|
+
try {
|
|
658
|
+
live
|
|
659
|
+
.prepare('UPDATE todo SET title = ?, _0_version = ? WHERE id = ?')
|
|
660
|
+
.run('new', '02', '1')
|
|
661
|
+
|
|
662
|
+
expect(snapshot.prepare('SELECT title FROM todo WHERE id = ?').get('1')).toEqual({
|
|
663
|
+
title: 'old',
|
|
664
|
+
})
|
|
665
|
+
} finally {
|
|
666
|
+
other.close()
|
|
667
|
+
}
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
it('persists BEGIN CONCURRENT writes for the zero-cache replica writer', () => {
|
|
671
|
+
const globalObject = globalThis as any
|
|
672
|
+
const previousRole = globalObject.__orez_zero_sqlite_role
|
|
673
|
+
globalObject.__orez_zero_sqlite_role = 'replica-writer'
|
|
674
|
+
let writer: Database
|
|
675
|
+
try {
|
|
676
|
+
writer = new Database(mock)
|
|
677
|
+
} finally {
|
|
678
|
+
if (previousRole === undefined) {
|
|
679
|
+
delete globalObject.__orez_zero_sqlite_role
|
|
680
|
+
} else {
|
|
681
|
+
globalObject.__orez_zero_sqlite_role = previousRole
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
writer.prepare('BEGIN CONCURRENT').run()
|
|
686
|
+
writer.prepare('INSERT INTO todo VALUES (?, ?, ?)').run('2', 'writer row', '02')
|
|
687
|
+
writer.prepare('COMMIT').run()
|
|
688
|
+
|
|
689
|
+
expect(live.prepare('SELECT title FROM todo WHERE id = ?').get('2')).toEqual({
|
|
690
|
+
title: 'writer row',
|
|
691
|
+
})
|
|
692
|
+
expect(
|
|
693
|
+
live
|
|
694
|
+
.prepare("SELECT name FROM sqlite_master WHERE name LIKE '_orez_snapshot_%'")
|
|
695
|
+
.all()
|
|
696
|
+
).toEqual([])
|
|
697
|
+
})
|
|
698
|
+
})
|
|
699
|
+
|
|
555
700
|
describe('StatementRunner: zero-cache replicator pattern', () => {
|
|
556
701
|
let mock: SqlStorageLike & { _nativeDb: any }
|
|
557
702
|
let db: Database
|
|
@@ -39,6 +39,9 @@ export interface SqlStorageLike {
|
|
|
39
39
|
transactionSync?<T>(fn: () => T): T
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
type SqliteConnectionRole = 'default' | 'replica-writer'
|
|
43
|
+
const activeSnapshotPrefixes = new Set<string>()
|
|
44
|
+
|
|
42
45
|
// -- SqliteError --
|
|
43
46
|
|
|
44
47
|
export class SqliteError extends Error {
|
|
@@ -109,6 +112,178 @@ function serializeSqliteParams(params: unknown[]): SqlStorageValue[] {
|
|
|
109
112
|
return params.map(serializeValue)
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
function quoteIdentifier(value: string): string {
|
|
116
|
+
return `"${value.replace(/"/g, '""')}"`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function snapshotTableName(prefix: string, table: string): string {
|
|
120
|
+
return `${prefix}_${table.replace(/[^A-Za-z0-9_]/g, '_')}`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isSnapshotInternalTable(name: string): boolean {
|
|
124
|
+
return name.startsWith('_orez_snapshot_')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function createSnapshotPrefix(): string {
|
|
128
|
+
const uuid =
|
|
129
|
+
typeof globalThis.crypto?.randomUUID === 'function'
|
|
130
|
+
? globalThis.crypto.randomUUID().replace(/-/g, '').slice(0, 16)
|
|
131
|
+
: Math.random().toString(36).slice(2, 18)
|
|
132
|
+
return `_orez_snapshot_${Date.now().toString(36)}_${uuid}`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isActiveSnapshotTable(name: string): boolean {
|
|
136
|
+
for (const prefix of activeSnapshotPrefixes) {
|
|
137
|
+
if (name.startsWith(`${prefix}_`)) return true
|
|
138
|
+
}
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function cleanupInactiveSnapshotTables(sql: SqlStorageLike): void {
|
|
143
|
+
try {
|
|
144
|
+
const rows = sql
|
|
145
|
+
.exec(
|
|
146
|
+
`SELECT name FROM sqlite_master
|
|
147
|
+
WHERE type = 'table'
|
|
148
|
+
AND name LIKE '_orez_snapshot_%'`
|
|
149
|
+
)
|
|
150
|
+
.toArray()
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
const name = String(row.name ?? '')
|
|
153
|
+
if (!isSnapshotInternalTable(name) || isActiveSnapshotTable(name)) continue
|
|
154
|
+
try {
|
|
155
|
+
sql.exec(`DROP TABLE IF EXISTS ${quoteIdentifier(name)}`)
|
|
156
|
+
} catch {}
|
|
157
|
+
}
|
|
158
|
+
} catch {}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function shouldSnapshotTable(name: string): boolean {
|
|
162
|
+
return (
|
|
163
|
+
name !== '__miniflare_do_name' &&
|
|
164
|
+
name !== 'storage' &&
|
|
165
|
+
name !== 'sqlite_stat1' &&
|
|
166
|
+
!isSnapshotInternalTable(name)
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function currentConnectionRole(): SqliteConnectionRole {
|
|
171
|
+
return (globalThis as any).__orez_zero_sqlite_role === 'replica-writer'
|
|
172
|
+
? 'replica-writer'
|
|
173
|
+
: 'default'
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isSqliteCatalogQuery(sql: string): boolean {
|
|
177
|
+
return /\bsqlite_(?:master|schema)\b/i.test(sql)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hasSnapshotCatalogName(row: Record<string, unknown>): boolean {
|
|
181
|
+
for (const key of ['name', 'tbl_name', 'table', 'tableName']) {
|
|
182
|
+
const value = row[key]
|
|
183
|
+
if (typeof value === 'string' && isSnapshotInternalTable(value)) return true
|
|
184
|
+
}
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function filterSnapshotCatalogRows<T>(sql: string, rows: T[]): T[] {
|
|
189
|
+
if (!isSqliteCatalogQuery(sql)) return rows
|
|
190
|
+
return rows.filter((row) => !hasSnapshotCatalogName(row as Record<string, unknown>))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function replaceIdentifierOutsideLiterals(
|
|
194
|
+
sql: string,
|
|
195
|
+
identifier: string,
|
|
196
|
+
replacement: string
|
|
197
|
+
): string {
|
|
198
|
+
let out = ''
|
|
199
|
+
let i = 0
|
|
200
|
+
|
|
201
|
+
while (i < sql.length) {
|
|
202
|
+
const ch = sql[i]
|
|
203
|
+
const next = sql[i + 1]
|
|
204
|
+
|
|
205
|
+
if (ch === "'") {
|
|
206
|
+
const start = i
|
|
207
|
+
i++
|
|
208
|
+
while (i < sql.length) {
|
|
209
|
+
if (sql[i] === "'" && sql[i + 1] === "'") {
|
|
210
|
+
i += 2
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
if (sql[i] === "'") {
|
|
214
|
+
i++
|
|
215
|
+
break
|
|
216
|
+
}
|
|
217
|
+
i++
|
|
218
|
+
}
|
|
219
|
+
out += sql.slice(start, i)
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (ch === '"') {
|
|
224
|
+
const start = i
|
|
225
|
+
let value = ''
|
|
226
|
+
i++
|
|
227
|
+
while (i < sql.length) {
|
|
228
|
+
if (sql[i] === '"' && sql[i + 1] === '"') {
|
|
229
|
+
value += '"'
|
|
230
|
+
i += 2
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
if (sql[i] === '"') {
|
|
234
|
+
i++
|
|
235
|
+
break
|
|
236
|
+
}
|
|
237
|
+
value += sql[i]
|
|
238
|
+
i++
|
|
239
|
+
}
|
|
240
|
+
out += value === identifier ? quoteIdentifier(replacement) : sql.slice(start, i)
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (ch === '-' && next === '-') {
|
|
245
|
+
const start = i
|
|
246
|
+
i += 2
|
|
247
|
+
while (i < sql.length && sql[i] !== '\n') i++
|
|
248
|
+
out += sql.slice(start, i)
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (ch === '/' && next === '*') {
|
|
253
|
+
const start = i
|
|
254
|
+
i += 2
|
|
255
|
+
while (i < sql.length && !(sql[i] === '*' && sql[i + 1] === '/')) i++
|
|
256
|
+
i = Math.min(sql.length, i + 2)
|
|
257
|
+
out += sql.slice(start, i)
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (/[A-Za-z_]/.test(ch)) {
|
|
262
|
+
const start = i
|
|
263
|
+
i++
|
|
264
|
+
while (i < sql.length && /[A-Za-z0-9_]/.test(sql[i])) i++
|
|
265
|
+
const word = sql.slice(start, i)
|
|
266
|
+
out += word === identifier ? replacement : word
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
out += ch
|
|
271
|
+
i++
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return out
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function rewriteSQLTables(sql: string, tables: Map<string, string>): string {
|
|
278
|
+
let rewritten = sql
|
|
279
|
+
const names = [...tables.keys()].sort((a, b) => b.length - a.length)
|
|
280
|
+
for (const name of names) {
|
|
281
|
+
const snapshot = tables.get(name)!
|
|
282
|
+
rewritten = replaceIdentifierOutsideLiterals(rewritten, name, snapshot)
|
|
283
|
+
}
|
|
284
|
+
return rewritten
|
|
285
|
+
}
|
|
286
|
+
|
|
112
287
|
// -- Statement --
|
|
113
288
|
|
|
114
289
|
export class Statement<T = Record<string, SqlStorageValue>> {
|
|
@@ -193,7 +368,9 @@ export class Statement<T = Record<string, SqlStorageValue>> {
|
|
|
193
368
|
upper.startsWith('ROLLBACK') ||
|
|
194
369
|
upper === 'END' ||
|
|
195
370
|
upper.startsWith('END ')
|
|
196
|
-
const
|
|
371
|
+
const resolved = this.#resolveParams(params)
|
|
372
|
+
const sql = this.#db._rewriteForSnapshot(resolved.sql)
|
|
373
|
+
const values = resolved.values
|
|
197
374
|
const cursor =
|
|
198
375
|
isTxCmd && values.length === 0
|
|
199
376
|
? this.#db._execTransactionAware(sql, this.#sql)
|
|
@@ -213,9 +390,11 @@ export class Statement<T = Record<string, SqlStorageValue>> {
|
|
|
213
390
|
return pragma.result.toArray()[0] as T
|
|
214
391
|
}
|
|
215
392
|
|
|
216
|
-
const
|
|
393
|
+
const resolved = this.#resolveParams(params)
|
|
394
|
+
const sql = this.#db._rewriteForSnapshot(resolved.sql)
|
|
395
|
+
const values = resolved.values
|
|
217
396
|
const cursor = this.#sql.exec(sql, ...values)
|
|
218
|
-
const rows = cursor.toArray()
|
|
397
|
+
const rows = filterSnapshotCatalogRows(sql, cursor.toArray())
|
|
219
398
|
return (rows[0] as T) ?? undefined
|
|
220
399
|
}
|
|
221
400
|
|
|
@@ -228,9 +407,11 @@ export class Statement<T = Record<string, SqlStorageValue>> {
|
|
|
228
407
|
return pragma.result.toArray() as T[]
|
|
229
408
|
}
|
|
230
409
|
|
|
231
|
-
const
|
|
410
|
+
const resolved = this.#resolveParams(params)
|
|
411
|
+
const sql = this.#db._rewriteForSnapshot(resolved.sql)
|
|
412
|
+
const values = resolved.values
|
|
232
413
|
const cursor = this.#sql.exec(sql, ...values)
|
|
233
|
-
return cursor.toArray() as T[]
|
|
414
|
+
return filterSnapshotCatalogRows(sql, cursor.toArray()) as T[]
|
|
234
415
|
}
|
|
235
416
|
|
|
236
417
|
iterate(...params: unknown[]): IterableIterator<T> {
|
|
@@ -271,7 +452,7 @@ export class Statement<T = Record<string, SqlStorageValue>> {
|
|
|
271
452
|
/** columns() returns column name metadata */
|
|
272
453
|
columns(): Array<{ name: string; column: string | null; table: string | null }> {
|
|
273
454
|
// execute a dummy query to get column names
|
|
274
|
-
const cursor = this.#sql.exec(this.source)
|
|
455
|
+
const cursor = this.#sql.exec(this.#db._rewriteForSnapshot(this.source))
|
|
275
456
|
return cursor.columnNames.map((name) => ({
|
|
276
457
|
name,
|
|
277
458
|
column: null,
|
|
@@ -295,6 +476,10 @@ export class Database {
|
|
|
295
476
|
#sql: SqlStorageLike
|
|
296
477
|
#open: boolean
|
|
297
478
|
#inTransaction: boolean
|
|
479
|
+
#snapshotTables: Map<string, string> | null
|
|
480
|
+
#snapshotPrefix: string
|
|
481
|
+
#snapshotCounter: number
|
|
482
|
+
#connectionRole: SqliteConnectionRole
|
|
298
483
|
|
|
299
484
|
constructor(sqlOrFilename: SqlStorageLike | string, _options?: { readonly?: boolean }) {
|
|
300
485
|
if (typeof sqlOrFilename === 'string') {
|
|
@@ -316,6 +501,12 @@ export class Database {
|
|
|
316
501
|
}
|
|
317
502
|
this.#open = true
|
|
318
503
|
this.#inTransaction = false
|
|
504
|
+
this.#snapshotTables = null
|
|
505
|
+
this.#snapshotPrefix = createSnapshotPrefix()
|
|
506
|
+
this.#snapshotCounter = 0
|
|
507
|
+
this.#connectionRole = currentConnectionRole()
|
|
508
|
+
activeSnapshotPrefixes.add(this.#snapshotPrefix)
|
|
509
|
+
cleanupInactiveSnapshotTables(this.#sql)
|
|
319
510
|
|
|
320
511
|
// expose storage for StatementRunner to access transactionSync
|
|
321
512
|
;(this as any).__orez_sql = this.#sql
|
|
@@ -348,6 +539,15 @@ export class Database {
|
|
|
348
539
|
|
|
349
540
|
// CF DO: all transaction control is no-op (DO coalesces writes automatically)
|
|
350
541
|
if (sqlStorage.transactionSync) {
|
|
542
|
+
if (upper.startsWith('BEGIN CONCURRENT')) {
|
|
543
|
+
if (this.#connectionRole === 'replica-writer') {
|
|
544
|
+
shared.__txDepth = (shared.__txDepth || 0) + 1
|
|
545
|
+
} else {
|
|
546
|
+
this.#beginSnapshot()
|
|
547
|
+
}
|
|
548
|
+
this.#inTransaction = true
|
|
549
|
+
return noopCursor
|
|
550
|
+
}
|
|
351
551
|
if (upper.startsWith('BEGIN')) {
|
|
352
552
|
shared.__txDepth = (shared.__txDepth || 0) + 1
|
|
353
553
|
this.#inTransaction = true
|
|
@@ -359,6 +559,7 @@ export class Database {
|
|
|
359
559
|
upper.startsWith('END ') ||
|
|
360
560
|
upper.startsWith('ROLLBACK')
|
|
361
561
|
) {
|
|
562
|
+
this.#dropSnapshot()
|
|
362
563
|
shared.__txDepth = Math.max(0, (shared.__txDepth || 0) - 1)
|
|
363
564
|
if (shared.__txDepth === 0) this.#inTransaction = false
|
|
364
565
|
return noopCursor
|
|
@@ -404,6 +605,47 @@ export class Database {
|
|
|
404
605
|
return sqlStorage.exec(sql)
|
|
405
606
|
}
|
|
406
607
|
|
|
608
|
+
#beginSnapshot(): void {
|
|
609
|
+
this.#dropSnapshot()
|
|
610
|
+
const prefix = `${this.#snapshotPrefix}_${++this.#snapshotCounter}`
|
|
611
|
+
const tables = new Map<string, string>()
|
|
612
|
+
const rows = this.#sql
|
|
613
|
+
.exec(
|
|
614
|
+
`SELECT name FROM sqlite_master
|
|
615
|
+
WHERE type = 'table'
|
|
616
|
+
ORDER BY name`
|
|
617
|
+
)
|
|
618
|
+
.toArray()
|
|
619
|
+
|
|
620
|
+
for (const row of rows) {
|
|
621
|
+
const name = String(row.name ?? '')
|
|
622
|
+
if (!shouldSnapshotTable(name)) continue
|
|
623
|
+
const snapshot = snapshotTableName(prefix, name)
|
|
624
|
+
this.#sql.exec(
|
|
625
|
+
`CREATE TABLE ${quoteIdentifier(snapshot)} AS SELECT * FROM ${quoteIdentifier(name)}`
|
|
626
|
+
)
|
|
627
|
+
tables.set(name, snapshot)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
this.#snapshotTables = tables
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
#dropSnapshot(): void {
|
|
634
|
+
const tables = this.#snapshotTables
|
|
635
|
+
if (!tables) return
|
|
636
|
+
this.#snapshotTables = null
|
|
637
|
+
for (const snapshot of tables.values()) {
|
|
638
|
+
try {
|
|
639
|
+
this.#sql.exec(`DROP TABLE IF EXISTS ${quoteIdentifier(snapshot)}`)
|
|
640
|
+
} catch {}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
_rewriteForSnapshot(sql: string): string {
|
|
645
|
+
if (!this.#snapshotTables) return sql
|
|
646
|
+
return rewriteSQLTables(sql, this.#snapshotTables)
|
|
647
|
+
}
|
|
648
|
+
|
|
407
649
|
/** prepare a statement */
|
|
408
650
|
prepare<T = Record<string, SqlStorageValue>>(sql: string): Statement<T> {
|
|
409
651
|
if (!this.#open) {
|
|
@@ -535,7 +777,7 @@ export class Database {
|
|
|
535
777
|
if (isTxCmd) {
|
|
536
778
|
this._execTransactionAware(stmt, this.#sql)
|
|
537
779
|
} else {
|
|
538
|
-
this.#sql.exec(stmt)
|
|
780
|
+
this.#sql.exec(this._rewriteForSnapshot(stmt))
|
|
539
781
|
}
|
|
540
782
|
}
|
|
541
783
|
|
|
@@ -603,6 +845,8 @@ export class Database {
|
|
|
603
845
|
|
|
604
846
|
/** close the database connection */
|
|
605
847
|
close(): this {
|
|
848
|
+
this.#dropSnapshot()
|
|
849
|
+
activeSnapshotPrefixes.delete(this.#snapshotPrefix)
|
|
606
850
|
this.#open = false
|
|
607
851
|
return this
|
|
608
852
|
}
|
|
@@ -697,6 +941,9 @@ export class StatementRunner {
|
|
|
697
941
|
}
|
|
698
942
|
|
|
699
943
|
beginConcurrent(): RunResult {
|
|
944
|
+
if (this.#storage?.transactionSync) {
|
|
945
|
+
return this.run('BEGIN CONCURRENT')
|
|
946
|
+
}
|
|
700
947
|
return this.begin()
|
|
701
948
|
}
|
|
702
949
|
|
|
@@ -709,14 +956,14 @@ export class StatementRunner {
|
|
|
709
956
|
|
|
710
957
|
commit(): RunResult {
|
|
711
958
|
if (this.#storage?.transactionSync) {
|
|
712
|
-
return this
|
|
959
|
+
return this.run('COMMIT')
|
|
713
960
|
}
|
|
714
961
|
return this.run('COMMIT')
|
|
715
962
|
}
|
|
716
963
|
|
|
717
964
|
rollback(): RunResult {
|
|
718
965
|
if (this.#storage?.transactionSync) {
|
|
719
|
-
return this
|
|
966
|
+
return this.run('ROLLBACK')
|
|
720
967
|
}
|
|
721
968
|
return this.run('ROLLBACK')
|
|
722
969
|
}
|
package/src/worker/shims/ws.ts
CHANGED
|
@@ -31,6 +31,35 @@ interface CFWebSocket {
|
|
|
31
31
|
accept?(): void
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function debugWS(event: Record<string, unknown>): void {
|
|
35
|
+
try {
|
|
36
|
+
const log = (globalThis as any).__orez_ws_debug_log
|
|
37
|
+
if (typeof log === 'function') log(event)
|
|
38
|
+
} catch {
|
|
39
|
+
// debug logging must never affect websocket behavior
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function summarizeWSData(data: unknown): Record<string, unknown> {
|
|
44
|
+
if (typeof data === 'string') {
|
|
45
|
+
return {
|
|
46
|
+
dataType: 'string',
|
|
47
|
+
bytes: data.length,
|
|
48
|
+
text: data.length > 300 ? `${data.slice(0, 300)}...` : data,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) {
|
|
52
|
+
return { dataType: 'buffer', bytes: data.byteLength }
|
|
53
|
+
}
|
|
54
|
+
if (data instanceof ArrayBuffer) {
|
|
55
|
+
return { dataType: 'arraybuffer', bytes: data.byteLength }
|
|
56
|
+
}
|
|
57
|
+
if (ArrayBuffer.isView(data)) {
|
|
58
|
+
return { dataType: data.constructor.name, bytes: data.byteLength }
|
|
59
|
+
}
|
|
60
|
+
return { dataType: typeof data }
|
|
61
|
+
}
|
|
62
|
+
|
|
34
63
|
// -- WebSocket shim --
|
|
35
64
|
// wraps a CF WebSocket to match the ws package WebSocket API
|
|
36
65
|
|
|
@@ -198,6 +227,7 @@ class WebSocket extends EventEmitter {
|
|
|
198
227
|
} else {
|
|
199
228
|
this.#ws = urlOrSocket
|
|
200
229
|
this.#url = ''
|
|
230
|
+
debugWS({ event: 'wrap-cf-socket', readyState: this.#ws.readyState })
|
|
201
231
|
this.#setupListeners()
|
|
202
232
|
}
|
|
203
233
|
}
|
|
@@ -215,6 +245,7 @@ class WebSocket extends EventEmitter {
|
|
|
215
245
|
cb?: (err?: Error) => void
|
|
216
246
|
): void {
|
|
217
247
|
try {
|
|
248
|
+
debugWS({ event: 'send', url: this.#url, ...summarizeWSData(data) })
|
|
218
249
|
if (typeof data === 'string') {
|
|
219
250
|
this.#ws.send(data)
|
|
220
251
|
} else if (Buffer.isBuffer(data)) {
|
|
@@ -299,15 +330,28 @@ class WebSocket extends EventEmitter {
|
|
|
299
330
|
// error: (err: Error)
|
|
300
331
|
const onMessage = (event: any) => {
|
|
301
332
|
const data = event.data
|
|
333
|
+
debugWS({ event: 'message', url: this.#url, ...summarizeWSData(data) })
|
|
302
334
|
this.emit('message', data, typeof data !== 'string')
|
|
303
335
|
}
|
|
304
336
|
const onClose = (event: any) => {
|
|
337
|
+
debugWS({
|
|
338
|
+
event: 'close',
|
|
339
|
+
url: this.#url,
|
|
340
|
+
code: event.code ?? 1000,
|
|
341
|
+
reason: event.reason ?? '',
|
|
342
|
+
})
|
|
305
343
|
this.emit('close', event.code ?? 1000, event.reason ?? '')
|
|
306
344
|
}
|
|
307
345
|
const onError = (event: any) => {
|
|
346
|
+
debugWS({
|
|
347
|
+
event: 'error',
|
|
348
|
+
url: this.#url,
|
|
349
|
+
message: event?.message ?? event?.error?.message ?? 'WebSocket error',
|
|
350
|
+
})
|
|
308
351
|
this.emit('error', event.error ?? new Error(event.message ?? 'WebSocket error'))
|
|
309
352
|
}
|
|
310
353
|
const onOpen = () => {
|
|
354
|
+
debugWS({ event: 'open', url: this.#url })
|
|
311
355
|
this.emit('open')
|
|
312
356
|
}
|
|
313
357
|
|
|
@@ -352,6 +396,7 @@ class WebSocketServer extends EventEmitter {
|
|
|
352
396
|
callback: (ws: WebSocket) => void
|
|
353
397
|
): void {
|
|
354
398
|
// wrap the CF WebSocket in our shim
|
|
399
|
+
debugWS({ event: 'handle-upgrade' })
|
|
355
400
|
const ws = new WebSocket(socket as CFWebSocket)
|
|
356
401
|
callback(ws)
|
|
357
402
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const globalProcess = ((globalThis as any).process ??= {})
|
|
2
|
+
|
|
3
|
+
globalProcess.env ??= {}
|
|
4
|
+
globalProcess.pid ??= 1
|
|
5
|
+
globalProcess.argv ??= []
|
|
6
|
+
globalProcess.kill ??= () => true
|
|
7
|
+
|
|
8
|
+
globalProcess.env.SINGLE_PROCESS = '1'
|
|
9
|
+
globalProcess.env.NODE_ENV ??= 'development'
|
|
10
|
+
|
|
11
|
+
export {}
|