orez 0.9.7 → 0.10.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.
Files changed (249) hide show
  1. package/README.md +27 -19
  2. package/dist/config.d.ts +0 -4
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +0 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/index.d.ts +0 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +5 -43
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -71
  11. package/dist/cf-deploy/bundle.d.ts +0 -21
  12. package/dist/cf-deploy/bundle.d.ts.map +0 -1
  13. package/dist/cf-deploy/bundle.js +0 -347
  14. package/dist/cf-deploy/bundle.js.map +0 -1
  15. package/dist/cf-deploy/config.d.ts +0 -52
  16. package/dist/cf-deploy/config.d.ts.map +0 -1
  17. package/dist/cf-deploy/config.js +0 -23
  18. package/dist/cf-deploy/config.js.map +0 -1
  19. package/dist/cf-deploy/index.d.ts +0 -11
  20. package/dist/cf-deploy/index.d.ts.map +0 -1
  21. package/dist/cf-deploy/index.js +0 -17
  22. package/dist/cf-deploy/index.js.map +0 -1
  23. package/dist/cf-deploy/leaves.d.ts +0 -19
  24. package/dist/cf-deploy/leaves.d.ts.map +0 -1
  25. package/dist/cf-deploy/leaves.js +0 -109
  26. package/dist/cf-deploy/leaves.js.map +0 -1
  27. package/dist/cf-deploy/migration.d.ts +0 -40
  28. package/dist/cf-deploy/migration.d.ts.map +0 -1
  29. package/dist/cf-deploy/migration.js +0 -891
  30. package/dist/cf-deploy/migration.js.map +0 -1
  31. package/dist/cf-deploy/nativeMigrations.d.ts +0 -24
  32. package/dist/cf-deploy/nativeMigrations.d.ts.map +0 -1
  33. package/dist/cf-deploy/nativeMigrations.js +0 -185
  34. package/dist/cf-deploy/nativeMigrations.js.map +0 -1
  35. package/dist/cf-deploy/prune.d.ts +0 -14
  36. package/dist/cf-deploy/prune.d.ts.map +0 -1
  37. package/dist/cf-deploy/prune.js +0 -235
  38. package/dist/cf-deploy/prune.js.map +0 -1
  39. package/dist/cf-deploy/shims.d.ts +0 -13
  40. package/dist/cf-deploy/shims.d.ts.map +0 -1
  41. package/dist/cf-deploy/shims.js +0 -737
  42. package/dist/cf-deploy/shims.js.map +0 -1
  43. package/dist/cf-deploy/smoke.d.ts +0 -15
  44. package/dist/cf-deploy/smoke.d.ts.map +0 -1
  45. package/dist/cf-deploy/smoke.js +0 -141
  46. package/dist/cf-deploy/smoke.js.map +0 -1
  47. package/dist/cf-deploy/sources.d.ts +0 -9
  48. package/dist/cf-deploy/sources.d.ts.map +0 -1
  49. package/dist/cf-deploy/sources.js +0 -276
  50. package/dist/cf-deploy/sources.js.map +0 -1
  51. package/dist/cf-deploy/wrangler.d.ts +0 -6
  52. package/dist/cf-deploy/wrangler.d.ts.map +0 -1
  53. package/dist/cf-deploy/wrangler.js +0 -168
  54. package/dist/cf-deploy/wrangler.js.map +0 -1
  55. package/dist/cf-do/application-sql.d.ts +0 -49
  56. package/dist/cf-do/application-sql.d.ts.map +0 -1
  57. package/dist/cf-do/application-sql.js +0 -124
  58. package/dist/cf-do/application-sql.js.map +0 -1
  59. package/dist/cf-do/cdc.d.ts +0 -155
  60. package/dist/cf-do/cdc.d.ts.map +0 -1
  61. package/dist/cf-do/cdc.js +0 -719
  62. package/dist/cf-do/cdc.js.map +0 -1
  63. package/dist/cf-do/cursor-pull.d.ts +0 -27
  64. package/dist/cf-do/cursor-pull.d.ts.map +0 -1
  65. package/dist/cf-do/cursor-pull.js +0 -78
  66. package/dist/cf-do/cursor-pull.js.map +0 -1
  67. package/dist/cf-do/row-undo.d.ts +0 -36
  68. package/dist/cf-do/row-undo.d.ts.map +0 -1
  69. package/dist/cf-do/row-undo.js +0 -221
  70. package/dist/cf-do/row-undo.js.map +0 -1
  71. package/dist/cf-do/tx-journal.d.ts +0 -119
  72. package/dist/cf-do/tx-journal.d.ts.map +0 -1
  73. package/dist/cf-do/tx-journal.js +0 -718
  74. package/dist/cf-do/tx-journal.js.map +0 -1
  75. package/dist/cf-do/watermark.d.ts +0 -29
  76. package/dist/cf-do/watermark.d.ts.map +0 -1
  77. package/dist/cf-do/watermark.js +0 -105
  78. package/dist/cf-do/watermark.js.map +0 -1
  79. package/dist/cf-do/worker.d.ts +0 -287
  80. package/dist/cf-do/worker.d.ts.map +0 -1
  81. package/dist/cf-do/worker.js +0 -1961
  82. package/dist/cf-do/worker.js.map +0 -1
  83. package/dist/do-sql-tracking.d.ts +0 -104
  84. package/dist/do-sql-tracking.d.ts.map +0 -1
  85. package/dist/do-sql-tracking.js +0 -255
  86. package/dist/do-sql-tracking.js.map +0 -1
  87. package/dist/pg-proxy-do-backend.d.ts +0 -211
  88. package/dist/pg-proxy-do-backend.d.ts.map +0 -1
  89. package/dist/pg-proxy-do-backend.js +0 -7884
  90. package/dist/pg-proxy-do-backend.js.map +0 -1
  91. package/dist/pg-sqlite-compiler/catalog/seed.d.ts +0 -67
  92. package/dist/pg-sqlite-compiler/catalog/seed.d.ts.map +0 -1
  93. package/dist/pg-sqlite-compiler/catalog/seed.js +0 -436
  94. package/dist/pg-sqlite-compiler/catalog/seed.js.map +0 -1
  95. package/dist/pg-sqlite-compiler/compiler.d.ts +0 -17
  96. package/dist/pg-sqlite-compiler/compiler.d.ts.map +0 -1
  97. package/dist/pg-sqlite-compiler/compiler.js +0 -86
  98. package/dist/pg-sqlite-compiler/compiler.js.map +0 -1
  99. package/dist/pg-sqlite-compiler/index.d.ts +0 -2
  100. package/dist/pg-sqlite-compiler/index.d.ts.map +0 -1
  101. package/dist/pg-sqlite-compiler/index.js +0 -13
  102. package/dist/pg-sqlite-compiler/index.js.map +0 -1
  103. package/dist/pg-sqlite-compiler/passes/array.d.ts +0 -3
  104. package/dist/pg-sqlite-compiler/passes/array.d.ts.map +0 -1
  105. package/dist/pg-sqlite-compiler/passes/array.js +0 -223
  106. package/dist/pg-sqlite-compiler/passes/array.js.map +0 -1
  107. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts +0 -48
  108. package/dist/pg-sqlite-compiler/passes/ast-utils.d.ts.map +0 -1
  109. package/dist/pg-sqlite-compiler/passes/ast-utils.js +0 -93
  110. package/dist/pg-sqlite-compiler/passes/ast-utils.js.map +0 -1
  111. package/dist/pg-sqlite-compiler/passes/catalog.d.ts +0 -34
  112. package/dist/pg-sqlite-compiler/passes/catalog.d.ts.map +0 -1
  113. package/dist/pg-sqlite-compiler/passes/catalog.js +0 -30
  114. package/dist/pg-sqlite-compiler/passes/catalog.js.map +0 -1
  115. package/dist/pg-sqlite-compiler/passes/datetime.d.ts +0 -23
  116. package/dist/pg-sqlite-compiler/passes/datetime.d.ts.map +0 -1
  117. package/dist/pg-sqlite-compiler/passes/datetime.js +0 -55
  118. package/dist/pg-sqlite-compiler/passes/datetime.js.map +0 -1
  119. package/dist/pg-sqlite-compiler/passes/dml-cte.d.ts +0 -22
  120. package/dist/pg-sqlite-compiler/passes/dml-cte.d.ts.map +0 -1
  121. package/dist/pg-sqlite-compiler/passes/dml-cte.js +0 -161
  122. package/dist/pg-sqlite-compiler/passes/dml-cte.js.map +0 -1
  123. package/dist/pg-sqlite-compiler/passes/index.d.ts +0 -24
  124. package/dist/pg-sqlite-compiler/passes/index.d.ts.map +0 -1
  125. package/dist/pg-sqlite-compiler/passes/index.js +0 -52
  126. package/dist/pg-sqlite-compiler/passes/index.js.map +0 -1
  127. package/dist/pg-sqlite-compiler/passes/json-functions.d.ts +0 -3
  128. package/dist/pg-sqlite-compiler/passes/json-functions.d.ts.map +0 -1
  129. package/dist/pg-sqlite-compiler/passes/json-functions.js +0 -57
  130. package/dist/pg-sqlite-compiler/passes/json-functions.js.map +0 -1
  131. package/dist/pg-sqlite-compiler/passes/row-json.d.ts +0 -3
  132. package/dist/pg-sqlite-compiler/passes/row-json.d.ts.map +0 -1
  133. package/dist/pg-sqlite-compiler/passes/row-json.js +0 -230
  134. package/dist/pg-sqlite-compiler/passes/row-json.js.map +0 -1
  135. package/dist/pg-sqlite-compiler/passes/schema.d.ts +0 -4
  136. package/dist/pg-sqlite-compiler/passes/schema.d.ts.map +0 -1
  137. package/dist/pg-sqlite-compiler/passes/schema.js +0 -128
  138. package/dist/pg-sqlite-compiler/passes/schema.js.map +0 -1
  139. package/dist/pg-sqlite-compiler/passes/string-functions.d.ts +0 -11
  140. package/dist/pg-sqlite-compiler/passes/string-functions.d.ts.map +0 -1
  141. package/dist/pg-sqlite-compiler/passes/string-functions.js +0 -37
  142. package/dist/pg-sqlite-compiler/passes/string-functions.js.map +0 -1
  143. package/dist/pg-sqlite-compiler/passes/types.d.ts +0 -41
  144. package/dist/pg-sqlite-compiler/passes/types.d.ts.map +0 -1
  145. package/dist/pg-sqlite-compiler/passes/types.js +0 -115
  146. package/dist/pg-sqlite-compiler/passes/types.js.map +0 -1
  147. package/dist/pg-sqlite-compiler/passes/unsupported.d.ts +0 -3
  148. package/dist/pg-sqlite-compiler/passes/unsupported.d.ts.map +0 -1
  149. package/dist/pg-sqlite-compiler/passes/unsupported.js +0 -57
  150. package/dist/pg-sqlite-compiler/passes/unsupported.js.map +0 -1
  151. package/dist/pg-sqlite-compiler/test/oracle.d.ts +0 -34
  152. package/dist/pg-sqlite-compiler/test/oracle.d.ts.map +0 -1
  153. package/dist/pg-sqlite-compiler/test/oracle.js +0 -204
  154. package/dist/pg-sqlite-compiler/test/oracle.js.map +0 -1
  155. package/dist/pg-sqlite-compiler/types.d.ts +0 -66
  156. package/dist/pg-sqlite-compiler/types.d.ts.map +0 -1
  157. package/dist/pg-sqlite-compiler/types.js +0 -2
  158. package/dist/pg-sqlite-compiler/types.js.map +0 -1
  159. package/dist/sync-browser-host/generated/sqlite3-browser.wasm +0 -0
  160. package/dist/sync-browser-host/generated/sync_wasm.js +0 -922
  161. package/dist/sync-browser-host/generated/sync_wasm_bg.wasm +0 -0
  162. package/dist/sync-browser-host/host.d.ts +0 -10
  163. package/dist/sync-browser-host/host.d.ts.map +0 -1
  164. package/dist/sync-browser-host/host.js +0 -496
  165. package/dist/sync-browser-host/host.js.map +0 -1
  166. package/dist/sync-browser-host/idb-snapshot.d.ts +0 -11
  167. package/dist/sync-browser-host/idb-snapshot.d.ts.map +0 -1
  168. package/dist/sync-browser-host/idb-snapshot.js +0 -122
  169. package/dist/sync-browser-host/idb-snapshot.js.map +0 -1
  170. package/dist/sync-browser-host/index.d.ts +0 -5
  171. package/dist/sync-browser-host/index.d.ts.map +0 -1
  172. package/dist/sync-browser-host/index.js +0 -5
  173. package/dist/sync-browser-host/index.js.map +0 -1
  174. package/dist/sync-browser-host/message-port.d.ts +0 -7
  175. package/dist/sync-browser-host/message-port.d.ts.map +0 -1
  176. package/dist/sync-browser-host/message-port.js +0 -188
  177. package/dist/sync-browser-host/message-port.js.map +0 -1
  178. package/dist/sync-browser-host/sqlite-adapter.d.ts +0 -67
  179. package/dist/sync-browser-host/sqlite-adapter.d.ts.map +0 -1
  180. package/dist/sync-browser-host/sqlite-adapter.js +0 -145
  181. package/dist/sync-browser-host/sqlite-adapter.js.map +0 -1
  182. package/dist/sync-browser-host/types.d.ts +0 -52
  183. package/dist/sync-browser-host/types.d.ts.map +0 -1
  184. package/dist/sync-browser-host/types.js +0 -2
  185. package/dist/sync-browser-host/types.js.map +0 -1
  186. package/dist/sync-cf-host.d.ts +0 -2
  187. package/dist/sync-cf-host.d.ts.map +0 -1
  188. package/dist/sync-cf-host.js +0 -2
  189. package/dist/sync-cf-host.js.map +0 -1
  190. package/dist/sync-executor.d.ts +0 -2
  191. package/dist/sync-executor.d.ts.map +0 -1
  192. package/dist/sync-executor.js +0 -2
  193. package/dist/sync-executor.js.map +0 -1
  194. package/dist/wake.d.ts +0 -22
  195. package/dist/wake.d.ts.map +0 -1
  196. package/dist/wake.js +0 -114
  197. package/dist/wake.js.map +0 -1
  198. package/dist/worker/cf-do-shim.d.ts +0 -55
  199. package/dist/worker/cf-do-shim.d.ts.map +0 -1
  200. package/dist/worker/cf-do-shim.js +0 -58
  201. package/dist/worker/cf-do-shim.js.map +0 -1
  202. package/dist/worker/cf-patches.d.ts +0 -43
  203. package/dist/worker/cf-patches.d.ts.map +0 -1
  204. package/dist/worker/cf-patches.js +0 -840
  205. package/dist/worker/cf-patches.js.map +0 -1
  206. package/dist/worker/local-sql-backend.d.ts +0 -35
  207. package/dist/worker/local-sql-backend.d.ts.map +0 -1
  208. package/dist/worker/local-sql-backend.js +0 -217
  209. package/dist/worker/local-sql-backend.js.map +0 -1
  210. package/dist/worker/zero-cache-embed-cf.d.ts +0 -92
  211. package/dist/worker/zero-cache-embed-cf.d.ts.map +0 -1
  212. package/dist/worker/zero-cache-embed-cf.js +0 -764
  213. package/dist/worker/zero-cache-embed-cf.js.map +0 -1
  214. package/dist/worker/zero-sql-write-circuit.d.ts +0 -46
  215. package/dist/worker/zero-sql-write-circuit.d.ts.map +0 -1
  216. package/dist/worker/zero-sql-write-circuit.js +0 -161
  217. package/dist/worker/zero-sql-write-circuit.js.map +0 -1
  218. package/dist/zero-http/encrypted-column-codec.d.ts +0 -46
  219. package/dist/zero-http/encrypted-column-codec.d.ts.map +0 -1
  220. package/dist/zero-http/encrypted-column-codec.js +0 -676
  221. package/dist/zero-http/encrypted-column-codec.js.map +0 -1
  222. package/dist/zero-http/encrypted-column-conformance.d.ts +0 -7
  223. package/dist/zero-http/encrypted-column-conformance.d.ts.map +0 -1
  224. package/dist/zero-http/encrypted-column-conformance.js +0 -161
  225. package/dist/zero-http/encrypted-column-conformance.js.map +0 -1
  226. package/dist/zero-http/fixture-schema.d.ts +0 -127
  227. package/dist/zero-http/fixture-schema.d.ts.map +0 -1
  228. package/dist/zero-http/fixture-schema.js +0 -56
  229. package/dist/zero-http/fixture-schema.js.map +0 -1
  230. package/dist/zero-http/mount.d.ts +0 -71
  231. package/dist/zero-http/mount.d.ts.map +0 -1
  232. package/dist/zero-http/mount.js +0 -594
  233. package/dist/zero-http/mount.js.map +0 -1
  234. package/dist/zero-http/payload-codec.d.ts +0 -91
  235. package/dist/zero-http/payload-codec.d.ts.map +0 -1
  236. package/dist/zero-http/payload-codec.js +0 -10
  237. package/dist/zero-http/payload-codec.js.map +0 -1
  238. package/dist/zero-http/server.d.ts +0 -16
  239. package/dist/zero-http/server.d.ts.map +0 -1
  240. package/dist/zero-http/server.js +0 -270
  241. package/dist/zero-http/server.js.map +0 -1
  242. package/dist/zero-http/test-harness.d.ts +0 -32
  243. package/dist/zero-http/test-harness.d.ts.map +0 -1
  244. package/dist/zero-http/test-harness.js +0 -77
  245. package/dist/zero-http/test-harness.js.map +0 -1
  246. package/dist/zero-http/transport.d.ts +0 -71
  247. package/dist/zero-http/transport.d.ts.map +0 -1
  248. package/dist/zero-http/transport.js +0 -1106
  249. package/dist/zero-http/transport.js.map +0 -1
@@ -1,891 +0,0 @@
1
- const SENTINEL_LOWER = 'nspfx';
2
- const SENTINEL_PASCAL = 'Nspfx';
3
- function applyPrefix(template, cfg) {
4
- return template
5
- .split(SENTINEL_PASCAL)
6
- .join(cfg.prefixPascal)
7
- .split(SENTINEL_LOWER)
8
- .join(cfg.prefix);
9
- }
10
- export function buildMigrationModuleSource(cfg, parts) {
11
- const runCloudflareMigrations = applyPrefix('runNspfxCloudflareMigrations', cfg);
12
- const migrationTableName = applyPrefix('__nspfx_cf_migrations', cfg);
13
- const sqlFetchGlobal = applyPrefix('__nspfx_cf_do_sql_fetch_by_instance', cfg);
14
- if (parts.mode === 'noop') {
15
- return `export const SCHEMA_VERSION = ${JSON.stringify(parts.schemaVersion)}\nexport async function ${runCloudflareMigrations}() {}\n`;
16
- }
17
- if (parts.mode === 'native') {
18
- const applicationSqlGlobal = applyPrefix('__nspfx_cf_application_sql_client', cfg);
19
- return `import { schema } from ${JSON.stringify(parts.schemaImportSpecifier)}
20
-
21
- export const SCHEMA_VERSION = ${JSON.stringify(parts.schemaVersion)}
22
-
23
- const nativeSqlStatements = ${JSON.stringify(parts.nativeSqlStatements)}
24
- const configuredPublicTables = ${JSON.stringify(parts.publicTables ?? [])}
25
- const expectedTables = ${JSON.stringify(parts.expectedTables ?? [])}
26
- const migrationTable = ${JSON.stringify(migrationTableName)}
27
-
28
- function quoteIdentifier(value) {
29
- return '"' + String(value).replaceAll('"', '""') + '"'
30
- }
31
-
32
- function schemaMetadataStatements() {
33
- return Object.values(schema.tables || {})
34
- .filter((table) => table && typeof table.name === 'string')
35
- .map((table) => ({
36
- sql: 'INSERT OR REPLACE INTO _zero_schema_tables (name, schema_json) VALUES (?, ?)',
37
- params: [
38
- table.name,
39
- JSON.stringify({ columns: table.columns, primaryKey: table.primaryKey }),
40
- ],
41
- }))
42
- }
43
-
44
- function publicTables() {
45
- if (configuredPublicTables.length) return configuredPublicTables
46
- return Object.values(schema.tables || {})
47
- .filter((table) => table && typeof table.name === 'string')
48
- .map((table) => {
49
- const publicTable = table.name.startsWith('public.') ? table.name : 'public.' + table.name
50
- return { table: publicTable.replace(/^public\\./, ''), publicTable }
51
- })
52
- }
53
-
54
- async function shouldSkipStatement(tx, statement) {
55
- if (statement.skipIfTableMissing) {
56
- const tempRows = await tx.query(
57
- 'PRAGMA table_info(' + quoteIdentifier(statement.skipIfTableMissing) + ')',
58
- )
59
- if (tempRows.length === 0) return true
60
- }
61
- const condition = statement.skipIfColumnExists || statement.skipIfColumnMissing
62
- if (!condition) return false
63
- const rows = await tx.query('PRAGMA table_info(' + quoteIdentifier(condition.table) + ')')
64
- const hasColumn = rows.some((row) => row && row.name === condition.column)
65
- return statement.skipIfColumnExists ? hasColumn : !hasColumn
66
- }
67
-
68
- function normalizeSqlType(value) {
69
- return String(value).trim().toLowerCase().replaceAll(/\\s+/g, ' ')
70
- }
71
-
72
- // every table's columns in ONE round trip. this used to be a PRAGMA
73
- // table_info per expected table in the shape assert plus another per ledgered
74
- // ADD COLUMN in the reconcile — order 125 sequential DO calls, all of them
75
- // holding the namespace's exclusive application-SQL session while concurrent
76
- // readers spin in the 25ms acquire retry. healing paths force this run on
77
- // EVERY request for an unseeded project, so its cost lands on exactly the
78
- // population it exists to repair (the 2026-07-21 outage shape).
79
- //
80
- // one behaviour difference from the PRAGMAs this replaced: lookups here are
81
- // case-EXACT, because the keys come from sqlite_master.name, while
82
- // PRAGMA table_info resolves a table name case-insensitively. No statement in
83
- // the current set mismatches, but a hand-written repair migration that spelled
84
- // a table name in a different case would read as a missing column, resurrect,
85
- // and then die on "duplicate column name". Match sqlite_master's spelling.
86
- async function readLiveColumns(tx) {
87
- // exclude protected/internal tables from the scan. once a Durable Object has
88
- // used its KV API, _cf_KV is listed in sqlite_master but the platform
89
- // authorizer refuses pragma_table_info on it, and one denied row fails the
90
- // WHOLE statement with SQLITE_AUTH (verified 2026-07-24 against a real DO:
91
- // scan passes without _cf_KV, fails the moment storage.put creates it, and
92
- // passes again with this filter). every prod namespace DO has _cf_KV, so the
93
- // unfiltered scan broke every migration session. caret escape on purpose:
94
- // this whole function body is emitted through a template literal, where a
95
- // literal backslash would need quadruple escaping.
96
- const rows = await tx.query(
97
- 'SELECT m.name AS tableName, p.name AS columnName, p.type AS columnType,' +
98
- ' p."notnull" AS columnNotNull, p.pk AS columnPk' +
99
- " FROM sqlite_master m JOIN pragma_table_info(m.name) p WHERE m.type = 'table'" +
100
- " AND m.name NOT LIKE '^_cf^_%' ESCAPE '^'" +
101
- " AND m.name NOT LIKE 'sqlite^_%' ESCAPE '^'",
102
- )
103
- const tables = new Map()
104
- for (const row of rows) {
105
- let columns = tables.get(row.tableName)
106
- if (!columns) {
107
- columns = []
108
- tables.set(row.tableName, columns)
109
- }
110
- columns.push({
111
- name: row.columnName,
112
- notnull: row.columnNotNull,
113
- pk: row.columnPk,
114
- type: row.columnType,
115
- })
116
- }
117
- return tables
118
- }
119
-
120
- async function assertExpectedSchema(tx) {
121
- const liveColumns = await readLiveColumns(tx)
122
- // two passes on purpose. reporting only the FIRST missing table makes an
123
- // operator fix them one deploy at a time, and a per-column mismatch later in
124
- // the list would throw before the missing-table set was even known. collect
125
- // every absent table first and report them together.
126
- const present = []
127
- const missingTables = []
128
- for (const expectedTable of expectedTables) {
129
- const actualColumns = liveColumns.get(expectedTable.name) || []
130
- if (actualColumns.length === 0) missingTables.push(expectedTable.name)
131
- else present.push({ actualColumns, expectedTable })
132
- }
133
- if (missingTables.length > 0) {
134
- throw new Error(
135
- 'application SQLite schema mismatch: missing table' +
136
- (missingTables.length > 1 ? 's' : '') +
137
- ' ' +
138
- missingTables.join(', '),
139
- )
140
- }
141
-
142
- for (const { actualColumns, expectedTable } of present) {
143
- const actualByName = new Map(actualColumns.map((column) => [column.name, column]))
144
- const missing = expectedTable.columns
145
- .filter((column) => !actualByName.has(column.name))
146
- .map((column) => column.name)
147
- if (missing.length > 0) {
148
- throw new Error(
149
- 'application SQLite schema mismatch for ' + expectedTable.name +
150
- ': missing [' + missing.join(', ') + ']',
151
- )
152
- }
153
-
154
- const expectedPrimaryKey = expectedTable.columns
155
- .filter((column) => column.primaryKeyOrder > 0)
156
- .sort((left, right) => left.primaryKeyOrder - right.primaryKeyOrder)
157
- .map((column) => column.name)
158
- const actualPrimaryKey = actualColumns
159
- .filter((column) => Number(column.pk) > 0)
160
- .sort((left, right) => Number(left.pk) - Number(right.pk))
161
- .map((column) => column.name)
162
- let equivalentPrimaryKeyIndex = false
163
- if (
164
- expectedPrimaryKey.length > 0 &&
165
- (actualPrimaryKey.length !== expectedPrimaryKey.length ||
166
- actualPrimaryKey.some((column, index) => column !== expectedPrimaryKey[index]))
167
- ) {
168
- const indexes = await tx.query(
169
- 'PRAGMA index_list(' + quoteIdentifier(expectedTable.name) + ')',
170
- )
171
- for (const index of indexes) {
172
- if (Number(index.unique) === 0 || Number(index.partial) !== 0) continue
173
- const columns = (await tx.query(
174
- 'PRAGMA index_info(' + quoteIdentifier(index.name) + ')',
175
- ))
176
- .sort((left, right) => Number(left.seqno) - Number(right.seqno))
177
- .map((column) => column.name)
178
- if (
179
- columns.length === expectedPrimaryKey.length &&
180
- columns.every((column, index) => column === expectedPrimaryKey[index])
181
- ) {
182
- equivalentPrimaryKeyIndex = true
183
- break
184
- }
185
- }
186
- }
187
-
188
- for (const expected of expectedTable.columns) {
189
- const actual = actualByName.get(expected.name)
190
- if (!actual) continue
191
- const actualType = normalizeSqlType(actual.type)
192
- const compatibleLegacyTimestamp =
193
- expected.sqlType === 'timestamp' &&
194
- (actualType === 'text' || actualType === 'real')
195
- const compatibleLegacyNumber =
196
- expected.sqlType === 'integer' &&
197
- (actualType === 'real' || actualType === 'text')
198
- if (
199
- actualType !== expected.sqlType &&
200
- !compatibleLegacyTimestamp &&
201
- !compatibleLegacyNumber
202
- ) {
203
- throw new Error(
204
- 'application SQLite schema mismatch for ' + expectedTable.name + '.' + expected.name +
205
- ': expected ' + expected.sqlType + ', found ' + (actualType || '(untyped)'),
206
- )
207
- }
208
-
209
- const actualNotNull = Number(actual.notnull) !== 0 || Number(actual.pk) !== 0
210
- if (actualNotNull !== expected.notNull) {
211
- if (expected.notNull && !actualNotNull) {
212
- const nullRows = await tx.query(
213
- 'SELECT 1 FROM ' + quoteIdentifier(expectedTable.name) +
214
- ' WHERE ' + quoteIdentifier(expected.name) + ' IS NULL LIMIT 1',
215
- )
216
- if (nullRows.length === 0) continue
217
- }
218
- throw new Error(
219
- 'application SQLite schema mismatch for ' + expectedTable.name + '.' + expected.name +
220
- ': expected ' + (expected.notNull ? 'NOT NULL' : 'nullable') +
221
- ', found ' + (actualNotNull ? 'NOT NULL' : 'nullable'),
222
- )
223
- }
224
- if (Number(actual.pk) !== expected.primaryKeyOrder && !equivalentPrimaryKeyIndex) {
225
- throw new Error(
226
- 'application SQLite schema mismatch for ' + expectedTable.name + '.' + expected.name +
227
- ': expected primary-key position ' + expected.primaryKeyOrder +
228
- ', found ' + actual.pk,
229
- )
230
- }
231
- }
232
- }
233
- }
234
-
235
- function hashMigrationSql(sql) {
236
- let h = 2166136261
237
- for (let i = 0; i < sql.length; i++) {
238
- h ^= sql.charCodeAt(i)
239
- h = Math.imul(h, 16777619)
240
- }
241
- return (h >>> 0).toString(36)
242
- }
243
-
244
- // a run killed mid-transaction (client abort, isolate eviction) rolls its DDL
245
- // back but keeps its ledger rows: the ledger is not a CDC-registered table, so
246
- // the DO's row undo never captures it. those phantom rows made every later run
247
- // skip statements whose effects no longer exist, permanently wedging the
248
- // namespace (2026-07-23 provisioning incident). before trusting the ledger,
249
- // delete entries whose effect is verifiably absent so their statements re-run
250
- // in order. a standalone DROP TABLE never re-runs: its effect IS absence.
251
- //
252
- // what this does NOT detect, stated plainly rather than as a reassurance:
253
- // - DML. a backfill re-runs only when its target table is gone entirely (the
254
- // same pass recreates it first). undone on a surviving table it is
255
- // invisible in schema and stays skipped, unlogged. the current set holds 50
256
- // UPDATE, 10 INSERT INTO and 6 DELETE FROM statements. this cannot wedge a
257
- // namespace, it leaves stale data behind a correct-looking schema.
258
- // - an EXTRA column. assertExpectedSchema catches a missing table, a missing
259
- // column, a type change and a NOT NULL change, but not a surplus one, so a
260
- // ledgered ALTER TABLE ... DROP COLUMN whose effect rolled back is neither
261
- // resurrected nor flagged.
262
- // - a column a later statement in the SAME file renames away. the
263
- // x__rebuild scratch columns in 20260723140000_declare_epoch_ms_integer
264
- // are added, copied, then renamed over the original, so the ADD COLUMN rule
265
- // always reads them as missing and re-adds them on a perfectly healthy
266
- // namespace. surplus columns, so assertExpectedSchema stays quiet. this
267
- // predates the block work and is not fixed here.
268
- // - a rebuild block that changes only nullability, defaults or foreign keys
269
- // without changing the column SET. its rollback leaves a table this pass
270
- // reads as landed. it does not silently pass: assertExpectedSchema fails
271
- // the run on the nullability difference, so the namespace reports loudly
272
- // instead of self-repairing.
273
- async function reconcilePhantomLedger(tx, applied) {
274
- if (applied.size === 0) return
275
- const schemaRows = await tx.query(
276
- "SELECT name, type FROM sqlite_master WHERE type IN ('table', 'index')",
277
- )
278
- const tables = new Set()
279
- const indexes = new Set()
280
- for (const row of schemaRows) {
281
- if (row.type === 'table') tables.add(row.name)
282
- else indexes.add(row.name)
283
- }
284
- const liveColumns = await readLiveColumns(tx)
285
- // a drizzle rebuild block (CREATE __new_<t> / INSERT..SELECT / DROP <t> /
286
- // RENAME) is ONE unit. its statements are individually meaningless, so they
287
- // resurrect together or not at all — including the DROP, which is the only
288
- // way the RENAME can land on a re-run. the honest evidence that the block
289
- // landed is <t> carrying the rebuilt COLUMN SET: a rolled-back block leaves
290
- // the ORIGINAL <t> in place, so judging it by table name reads every wedged
291
- // namespace as healthy and silently repairs nothing.
292
- const blockKey = (item) => item.id.split(':')[0] + '::' + item.rebuildTarget
293
- const blockLanded = new Map()
294
- for (const statement of nativeSqlStatements) {
295
- if (!statement || typeof statement !== 'object') continue
296
- if (!statement.rebuildTarget || !statement.rebuildColumns) continue
297
- const key = blockKey(statement)
298
- if (blockLanded.has(key)) continue
299
- const actualNames = new Set(
300
- (liveColumns.get(statement.rebuildTarget) || []).map((column) => column.name),
301
- )
302
- // a superset test, not equality: a later migration legitimately adds
303
- // columns to <t>, and treating that as "not landed" would drop a live
304
- // table and rebuild it from a shape that no longer has them.
305
- blockLanded.set(
306
- key,
307
- actualNames.size > 0 &&
308
- statement.rebuildColumns.every((column) => actualNames.has(column)),
309
- )
310
- }
311
- // the tables a resurrecting block will DROP and recreate this pass.
312
- const rebuiltTables = new Set()
313
- for (const statement of nativeSqlStatements) {
314
- if (!statement || typeof statement !== 'object' || !statement.rebuildTarget) continue
315
- if (blockLanded.get(blockKey(statement)) === false) {
316
- rebuiltTables.add(statement.rebuildTarget)
317
- }
318
- }
319
- const resurrect = new Set()
320
- for (const [index, statement] of nativeSqlStatements.entries()) {
321
- const item = typeof statement === 'string'
322
- ? { id: 'statement-' + index, sql: statement }
323
- : statement
324
- if (!item || typeof item.sql !== 'string' || !item.sql.trim()) continue
325
- const baseId = typeof item.id === 'string' && item.id ? item.id : 'statement-' + index
326
- let ledgered = false
327
- for (const id of applied) {
328
- if (id === baseId || id.startsWith(baseId + ':')) {
329
- ledgered = true
330
- break
331
- }
332
- }
333
- if (!ledgered) continue
334
- // block members bypass the per-statement rules entirely: their effect is
335
- // the block's effect, judged once above.
336
- if (item.rebuildTarget) {
337
- if (blockLanded.get(blockKey(item)) === false) resurrect.add(baseId)
338
- continue
339
- }
340
- const sql = item.sql.trim()
341
- let match
342
- let missing = false
343
- if ((match = /^CREATE TABLE\\s+(?:IF NOT EXISTS\\s+)?[\`"]?(\\w+)/i.exec(sql))) {
344
- missing = !tables.has(match[1])
345
- // a ledgered CREATE trusted by NAME can hide a table that predates the
346
- // CREATE's definition: the table resolves, the CREATE never re-runs, and
347
- // columns that only exist inside its definition are unreachable — every
348
- // later index on them then fails the whole run, forever (prod
349
- // 2026-07-26, 217 wedged namespaces retrying every sweep). converge the
350
- // live table to the declared column set instead. an empty live column
351
- // read is no evidence and must not trigger blind ALTERs.
352
- if (!missing && Array.isArray(item.declaredColumns)) {
353
- const live = new Set(
354
- (liveColumns.get(match[1]) || []).map((column) => column.name),
355
- )
356
- if (live.size > 0) {
357
- for (const column of item.declaredColumns) {
358
- if (!column || typeof column.name !== 'string') continue
359
- if (live.has(column.name)) continue
360
- let definition = String(column.definition || '').trim()
361
- if (!definition) continue
362
- // sqlite refuses ADD COLUMN for these shapes; leave them to
363
- // assertExpectedSchema, which reports the namespace loudly.
364
- if (/\\bPRIMARY KEY\\b|\\bUNIQUE\\b/i.test(definition)) continue
365
- if (/\\bNOT NULL\\b/i.test(definition) && /\\bREFERENCES\\b/i.test(definition)) continue
366
- if (/\\bNOT NULL\\b/i.test(definition) && !/\\bDEFAULT\\b/i.test(definition)) {
367
- // assertExpectedSchema compares name, type and NOT NULL but not
368
- // defaults, so a zero default converges without a false mismatch.
369
- definition += /\\b(?:INT|INTEGER|REAL|NUMERIC|BOOLEAN|FLOAT|DOUBLE|DECIMAL)\\b/i.test(definition)
370
- ? ' DEFAULT 0'
371
- : " DEFAULT ''"
372
- }
373
- console.warn(
374
- '[orez-migrations] converging ' + match[1] + '.' + column.name +
375
- ' from ledgered ' + baseId,
376
- )
377
- await tx.exec(
378
- 'ALTER TABLE ' + quoteIdentifier(match[1]) + ' ADD COLUMN ' + definition,
379
- )
380
- }
381
- }
382
- }
383
- } else if ((match = /^CREATE (?:UNIQUE )?INDEX\\s+(?:IF NOT EXISTS\\s+)?[\`"]?(\\w+)[\`"]?\\s+ON\\s+[\`"]?(\\w+)/i.exec(sql))) {
384
- // the index set was sampled BEFORE this pass drops and rebuilds <t>, so
385
- // "the index exists" is stale evidence for any table a resurrecting
386
- // block is about to DROP: the drop takes its indexes with it and the
387
- // trailing CREATE INDEXes stay ledgered, leaving the rebuilt table
388
- // bare. that silently removed the UNIQUE index invite redemption's
389
- // ON CONFLICT target needs.
390
- missing = !indexes.has(match[1]) || rebuiltTables.has(match[2])
391
- } else if ((match = /^ALTER TABLE\\s+[\`"]?(\\w+)[\`"]?\\s+RENAME TO\\s+[\`"]?(\\w+)/i.exec(sql))) {
392
- missing = !tables.has(match[2])
393
- } else if ((match = /^ALTER TABLE\\s+[\`"]?(\\w+)[\`"]?\\s+ADD\\s+(?:COLUMN\\s+)?[\`"]?(\\w+)/i.exec(sql))) {
394
- const columns = liveColumns.get(match[1])
395
- missing = !columns || !columns.some((column) => column.name === match[2])
396
- } else if ((match = /^(?:INSERT INTO|UPDATE|DELETE FROM|ALTER TABLE)\\s+[\`"]?(\\w+)/i.exec(sql))) {
397
- missing = !tables.has(match[1])
398
- }
399
- if (missing) resurrect.add(baseId)
400
- }
401
- if (resurrect.size === 0) return
402
- // a resurrected rebuild block must re-run with its sibling PRAGMA
403
- // foreign_keys toggles, or the block re-executes under FK enforcement.
404
- const resurrectedFiles = new Set(
405
- [...resurrect].map((baseId) => baseId.split(':')[0]),
406
- )
407
- for (const [index, statement] of nativeSqlStatements.entries()) {
408
- const item = typeof statement === 'string'
409
- ? { id: 'statement-' + index, sql: statement }
410
- : statement
411
- if (!item || typeof item.sql !== 'string') continue
412
- if (!/^\\s*PRAGMA\\b/i.test(item.sql.trim())) continue
413
- const baseId = typeof item.id === 'string' && item.id ? item.id : 'statement-' + index
414
- if (resurrectedFiles.has(baseId.split(':')[0])) resurrect.add(baseId)
415
- }
416
- console.warn(
417
- '[orez-migrations] resurrecting ' + resurrect.size +
418
- ' phantom ledger entries: ' + [...resurrect].join(', '),
419
- )
420
- // clear a leftover __new_<t> before the block replays, but ONLY when <t> is
421
- // also present. both present means the previous run died before its DROP, so
422
- // __new_<t> is a stale partial copy and the resurrected INSERT..SELECT would
423
- // fail on rows already in it ("UNIQUE constraint failed: __new_<t>.id").
424
- // <t> ABSENT is the opposite case and must not be touched: there the DROP did
425
- // land, __new_<t> holds the only copy of the rows, and the replay recreates an
426
- // empty <t>, copies nothing out of it, drops it and renames __new_<t> over it.
427
- for (const table of rebuiltTables) {
428
- if (!tables.has(table) || !tables.has('__new_' + table)) continue
429
- await tx.exec('DROP TABLE IF EXISTS ' + quoteIdentifier('__new_' + table))
430
- }
431
- for (const baseId of resurrect) {
432
- for (const id of [...applied]) {
433
- if (id !== baseId && !id.startsWith(baseId + ':')) continue
434
- await tx.exec(
435
- 'DELETE FROM ' + quoteIdentifier(migrationTable) + ' WHERE id = ?',
436
- [id],
437
- )
438
- applied.delete(id)
439
- }
440
- }
441
- }
442
-
443
- async function applyNativeSchema(tx, instance) {
444
- await tx.exec(
445
- 'CREATE TABLE IF NOT EXISTS ' + quoteIdentifier(migrationTable) +
446
- ' (id TEXT PRIMARY KEY, applied_at INTEGER NOT NULL)',
447
- )
448
- const appliedRows = await tx.query('SELECT id FROM ' + quoteIdentifier(migrationTable))
449
- const applied = new Set(appliedRows.map((row) => row.id))
450
- await reconcilePhantomLedger(tx, applied)
451
- // register BEFORE the statements, not only after them. a cdc trigger whose
452
- // _orez_cdc_tables row is missing is invisible to beginSchemaChange, which
453
- // skips unregistered tables, so it never suspends capture and the next
454
- // DROP/RENAME COLUMN on that table dies on the surviving trigger. ensureTable
455
- // reads that orphan as changed (its registration lookup returns nothing),
456
- // drops the stale triggers and rewrites the row, so registering first is what
457
- // lets the DDL below suspend capture at all. the pass after the statements
458
- // still runs and still owns publication; this one only heals. on a fresh
459
- // namespace every table is absent, ensureTable returns false for each, and
460
- // this is a no-op beyond creating the cdc bookkeeping tables early.
461
- await tx.registerTables(publicTables())
462
- const appliedIds = [...applied]
463
- const appliedStatementIds = new Set()
464
- const supersededStatementIds = new Set()
465
- for (const [index, statement] of nativeSqlStatements.entries()) {
466
- const item = typeof statement === 'string'
467
- ? { id: 'statement-' + index, sql: statement }
468
- : statement
469
- if (!item || typeof item.sql !== 'string' || !item.sql.trim()) continue
470
- const baseId = typeof item.id === 'string' && item.id ? item.id : 'statement-' + index
471
- if (
472
- applied.has(baseId) ||
473
- appliedIds.some((appliedId) => appliedId.startsWith(baseId + ':'))
474
- ) {
475
- appliedStatementIds.add(baseId)
476
- }
477
- for (const id of Array.isArray(item.supersedes) ? item.supersedes : []) {
478
- supersededStatementIds.add(id)
479
- }
480
- }
481
- // the ledger table is treated as replication bookkeeping by the DO backend,
482
- // so its writes are NOT covered by the transaction rollback: an aborted run
483
- // used to leave every already-recorded statement marked applied while its
484
- // data and DDL effects were undone, and later runs then skipped work that
485
- // never happened. compensate explicitly: on failure, delete the rows this
486
- // run inserted before rethrowing.
487
- const insertedThisRun = []
488
- try {
489
- for (const [index, statement] of nativeSqlStatements.entries()) {
490
- const item = typeof statement === 'string'
491
- ? { id: 'statement-' + index, sql: statement }
492
- : statement
493
- if (!item || typeof item.sql !== 'string' || !item.sql.trim()) continue
494
- // a statement that is only sql comments (a supersession-anchor
495
- // migration) has nothing to execute.
496
- if (!item.sql.split('\\n').some((line) => {
497
- const trimmed = line.trim()
498
- return trimmed.length > 0 && !trimmed.startsWith('--')
499
- })) continue
500
- const baseId = typeof item.id === 'string' && item.id ? item.id : 'statement-' + index
501
- if (supersededStatementIds.has(baseId)) continue
502
- const id = baseId + ':' + hashMigrationSql(item.sql)
503
- // migration statement ids are immutable execution identities. the SQL hash
504
- // records which source version ran, but editing an old migration must never
505
- // make destructive SQL run again under the same id.
506
- if (appliedStatementIds.has(baseId)) continue
507
- if (!(await shouldSkipStatement(tx, item))) {
508
- try {
509
- await tx.exec(item.sql, Array.isArray(item.params) ? item.params : [])
510
- } catch (error) {
511
- // a bare sqlite message ("no such table: x") names neither the failing
512
- // statement nor which of its siblings the ledger already recorded, and
513
- // that ledger state is the only thing that explains a half-applied
514
- // table-recreate block. the live column list of the target table is
515
- // the other half of that story for DDL chains.
516
- const file = baseId.split(':')[0]
517
- const siblings = [...applied].filter((appliedId) => appliedId.startsWith(file))
518
- let tableInfo = ''
519
- const target = /(?:UPDATE|ALTER TABLE|DELETE FROM|INSERT INTO)\\s+[\`"]?(\\w+)/i.exec(item.sql)
520
- if (target) {
521
- try {
522
- const columns = await tx.query('PRAGMA table_info(' + quoteIdentifier(target[1]) + ')')
523
- tableInfo = ' | table_info(' + target[1] + '): ' +
524
- columns.map((column) => column.name + ':' + column.type).join(', ')
525
- } catch {}
526
- }
527
- throw new Error(
528
- 'migration statement ' + baseId + ' failed on instance ' + instance + ': ' +
529
- (error && error.message ? error.message : String(error)) +
530
- ' | sql: ' + item.sql.replace(/\\s+/g, ' ').slice(0, 160) +
531
- ' | ledger for ' + file + ': ' + (siblings.join(', ') || '(none)') +
532
- tableInfo,
533
- { cause: error },
534
- )
535
- }
536
- }
537
- if (!applied.has(id)) {
538
- await tx.exec(
539
- 'INSERT INTO ' + quoteIdentifier(migrationTable) + ' (id, applied_at) VALUES (?, ?)',
540
- [id, Date.now()],
541
- )
542
- applied.add(id)
543
- appliedStatementIds.add(baseId)
544
- insertedThisRun.push(id)
545
- }
546
- }
547
- } catch (error) {
548
- for (const insertedId of insertedThisRun) {
549
- try {
550
- await tx.exec(
551
- 'DELETE FROM ' + quoteIdentifier(migrationTable) + ' WHERE id = ?',
552
- [insertedId],
553
- )
554
- } catch {}
555
- }
556
- throw error
557
- }
558
- await assertExpectedSchema(tx)
559
- await tx.exec(
560
- 'CREATE TABLE IF NOT EXISTS _zero_schema_tables (name TEXT PRIMARY KEY, schema_json TEXT NOT NULL)',
561
- )
562
- try {
563
- for (const statement of schemaMetadataStatements()) {
564
- await tx.exec(statement.sql, statement.params)
565
- }
566
- await tx.registerTables(publicTables())
567
- } catch (error) {
568
- // publication runs against every registered table, so a table this schema
569
- // no longer has still gets queried here. without the label the error is
570
- // indistinguishable from a migration statement failing.
571
- throw new Error(
572
- 'schema publication failed on instance ' + instance + ': ' +
573
- (error && error.message ? error.message : String(error)),
574
- { cause: error },
575
- )
576
- }
577
- }
578
-
579
- // what the namespace actually looks like, for a failure message. the single
580
- // most expensive missing fact when a namespace is wedged is whether the table
581
- // the error names is genuinely absent or merely unreachable — a whole day went
582
- // into guessing that on 2026-07-23 because nothing ever said.
583
- //
584
- // runs on the error path only, in its own session, and must never throw: a
585
- // namespace too broken to answer is exactly the case it exists to describe.
586
- async function liveSchemaSummary(client) {
587
- try {
588
- let summary = ''
589
- await client.transaction(() => {
590
- throw new Error('native schema migration does not use queryAst')
591
- }, async (tx) => {
592
- const rows = await tx.query(
593
- "SELECT name FROM sqlite_master WHERE type = 'table'" +
594
- " AND name NOT LIKE '^_cf^_%' ESCAPE '^'" +
595
- " AND name NOT LIKE 'sqlite^_%' ESCAPE '^' ORDER BY name",
596
- )
597
- const present = new Set(rows.map((row) => row.name))
598
- const missing = expectedTables
599
- .map((table) => table.name)
600
- .filter((name) => !present.has(name))
601
- summary =
602
- ' | live tables: ' + present.size +
603
- (missing.length ? ', MISSING expected: ' + missing.join(', ') : ', none missing')
604
- })
605
- return summary
606
- } catch (error) {
607
- return ' | live schema unreadable: ' + (error && error.message ? error.message : String(error))
608
- }
609
- }
610
-
611
- // \`client\` lets a caller that is ALREADY inside the durable object hand in an
612
- // in-process client (ZeroDO#applicationSqlLocalClient) instead of one that
613
- // round-trips per statement. it is a parameter rather than another global
614
- // because durable object instances share an isolate, so a global bound to one
615
- // instance would be read by another.
616
- export async function ${runCloudflareMigrations}({
617
- schemaOnly = false,
618
- registrationOnly = false,
619
- instance = 'singleton',
620
- client: providedClient,
621
- } = {}) {
622
- if (schemaOnly && registrationOnly) {
623
- throw new Error('schemaOnly and registrationOnly are mutually exclusive')
624
- }
625
- let client = providedClient
626
- if (!client) {
627
- const createClient = globalThis.${applicationSqlGlobal}
628
- if (typeof createClient !== 'function') {
629
- throw new Error('Cloudflare application SQLite client is not initialized')
630
- }
631
- client = createClient(instance)
632
- }
633
- if (registrationOnly) {
634
- await client.registerTables(publicTables())
635
- return { tables: publicTables().map((table) => table.publicTable) }
636
- }
637
- // the statement loop labels its own failures; acquiring the session and
638
- // committing sit OUTSIDE every try/catch in applyNativeSchema, so a Durable
639
- // Object SQL error raised there arrives bare. the schema barrier hands that
640
- // string straight back as a 503 body, which is the entire diagnosis budget a
641
- // wedged namespace gets — prod has answered \`no such table: main.user:
642
- // SQLITE_ERROR\` since 2026-07-23 and it names neither the phase that raised
643
- // it nor whether that table is really gone. the phantom-ledger reconcile
644
- // heals a dropped table correctly (verified against a real durable object),
645
- // so what is left is only reachable through a message that says more.
646
- let phase = 'session-acquire'
647
- try {
648
- await client.transaction(() => {
649
- throw new Error('native schema migration does not use queryAst')
650
- }, async (tx) => {
651
- phase = 'reconcile'
652
- await applyNativeSchema(tx, instance)
653
- phase = 'commit'
654
- })
655
- } catch (error) {
656
- throw new Error(
657
- 'schema migration failed on instance ' + instance + ' during ' + phase + ': ' +
658
- (error && error.message ? error.message : String(error)) +
659
- (await liveSchemaSummary(client)),
660
- { cause: error },
661
- )
662
- }
663
- return {
664
- tables: publicTables().map((table) => table.publicTable),
665
- ...(schemaOnly ? { schemaOnly: true } : null),
666
- }
667
- }
668
- `;
669
- }
670
- const imports = [
671
- `import { Pool } from 'pg'`,
672
- `import { schema } from ${JSON.stringify(parts.schemaImportSpecifier)}`,
673
- ...parts.migrationFiles.map((file, index) => `import * as migration${index} from ${JSON.stringify(file.importSpecifier)}`),
674
- ];
675
- const migrations = parts.migrationFiles
676
- .map((file, index) => {
677
- return `{ id: ${JSON.stringify(file.id)}, up: migration${index}.up }`;
678
- })
679
- .join(',\n ');
680
- return `${imports.join('\n')}
681
-
682
- export const SCHEMA_VERSION = ${JSON.stringify(parts.schemaVersion)}
683
-
684
- const migrations = [
685
- ${migrations}
686
- ].filter((migration) => typeof migration.up === 'function')
687
-
688
- const migrationTable = '${migrationTableName}'
689
-
690
- // regenerated-from-schema DDL (CREATE TABLE IF NOT EXISTS ...). always current
691
- // vs the drizzle migrations dir, which the in-browser codegen never rewrites.
692
- const initSql = ${JSON.stringify(parts.initSql)}
693
- // the same DDL pre-translated at deploy time by orez's statement rewriter
694
- // (deployTimeSchemaBatchStatements): SQLite-native DDL + the _orez_pg_metadata
695
- // upserts that give the data tier its pg column types. /batch-applied with NO
696
- // runtime libpg parse.
697
- const initSqlBatchStatements = ${JSON.stringify(parts.initSqlBatchStatements)}
698
- // zero-http uses the zero shard for clientGroupID ownership and mutation
699
- // results. this repairs old persistent CF namespaces and creates the same
700
- // shape for fresh namespaces before the first push.
701
- const zeroHttpShardSql = ${JSON.stringify(parts.zeroHttpShardSql)}
702
- const zeroHttpShardBatchStatements = ${JSON.stringify(parts.zeroHttpShardBatchStatements)}
703
-
704
- function quoteIdentifier(value) {
705
- return '"' + String(value).replaceAll('"', '""') + '"'
706
- }
707
-
708
- // apply the full schema as ONE /batch of SQLite-native DDL straight to the SQL
709
- // DO (ctx.storage.sql.exec, no parse, single transaction). this replaces ~118
710
- // per-statement /exec calls, each of which paid the DoBackend client's
711
- // libpg-query WASM parse (~1.4s) — the cold-migration loop that hit 36s + OOM.
712
- // the DDL is IF-NOT-EXISTS so re-runs are no-ops. falls back to the node pg path
713
- // off-CF (no per-instance DO fetch registry).
714
- async function applyInitSqlDDL(client, instance) {
715
- const schemaMetadataStatements = Object.values(schema.tables || {})
716
- .filter((table) => table && typeof table.name === 'string')
717
- .map((table) => ({
718
- sql: 'INSERT OR REPLACE INTO _zero_schema_tables (name, schema_json) VALUES (?, ?)',
719
- params: [
720
- table.name,
721
- JSON.stringify({ columns: table.columns, primaryKey: table.primaryKey }),
722
- ],
723
- }))
724
- const batchStatements = [
725
- ...initSqlBatchStatements,
726
- ...zeroHttpShardBatchStatements,
727
- {
728
- sql: 'CREATE TABLE IF NOT EXISTS _zero_schema_tables (name TEXT PRIMARY KEY, schema_json TEXT NOT NULL)',
729
- },
730
- ...schemaMetadataStatements,
731
- ]
732
- const fetch = (globalThis.${sqlFetchGlobal} || {})[
733
- instance || 'singleton'
734
- ]
735
- if (typeof fetch === 'function') {
736
- if (batchStatements.length === 0) return
737
- const res = await fetch('https://orez-do-backend.local/batch', {
738
- method: 'POST',
739
- headers: { 'content-type': 'application/json' },
740
- body: JSON.stringify({ statements: batchStatements }),
741
- })
742
- if (!res.ok) {
743
- throw new Error('schema /batch failed: ' + res.status + ' ' + (await res.text()))
744
- }
745
- return
746
- }
747
- for (const sql of [initSql, zeroHttpShardSql]) {
748
- for (const statement of sql
749
- .split('--> statement-breakpoint')
750
- .map((statement) => statement.trim())
751
- .filter((statement) => statement.length > 0)) {
752
- await client.query(statement)
753
- }
754
- }
755
- }
756
-
757
- function tableNamesFromSchema() {
758
- return Object.values(schema.tables || {})
759
- .map((table) => table && table.name)
760
- .filter((name) => typeof name === 'string' && name.length > 0)
761
- }
762
-
763
- async function ensurePublication(client, publications) {
764
- const publicationName = publications[0]
765
- const schemaTables = tableNamesFromSchema()
766
- if (!publicationName || schemaTables.length === 0) return []
767
-
768
- // only publish tables that actually exist. a zero-schema table with no DDL
769
- // must not abort the whole publication (CREATE/ALTER PUBLICATION throws on a
770
- // missing relation), which would take every other table offline too. with
771
- // applyInitSqlDDL run first this is normally a no-op, but it keeps a single
772
- // drifted table from breaking all of replication.
773
- const existingTablesRes = await client.query(
774
- "SELECT tablename FROM pg_tables WHERE schemaname = 'public'",
775
- )
776
- const existingTables = new Set(existingTablesRes.rows.map((row) => row.tablename))
777
- const tableNames = schemaTables.filter((table) => existingTables.has(table))
778
- if (tableNames.length === 0) return []
779
-
780
- const existing = await client.query(
781
- 'SELECT 1 FROM pg_publication WHERE pubname = $1',
782
- [publicationName],
783
- )
784
- if (!existing.rows.length) {
785
- await client.query(
786
- 'CREATE PUBLICATION ' +
787
- quoteIdentifier(publicationName) +
788
- ' FOR TABLE ' +
789
- tableNames.map(quoteIdentifier).join(', '),
790
- )
791
- return tableNames
792
- }
793
-
794
- const current = await client.query(
795
- "SELECT tablename FROM pg_publication_tables WHERE pubname = $1 AND schemaname = 'public'",
796
- [publicationName],
797
- )
798
- const currentTables = new Set(current.rows.map((row) => row.tablename))
799
- const missing = tableNames.filter((table) => !currentTables.has(table))
800
- if (missing.length) {
801
- await client.query(
802
- 'ALTER PUBLICATION ' +
803
- quoteIdentifier(publicationName) +
804
- ' ADD TABLE ' +
805
- missing.map(quoteIdentifier).join(', '),
806
- )
807
- }
808
- return tableNames
809
- }
810
-
811
- export async function ${runCloudflareMigrations}({
812
- publications = [],
813
- schemaOnly = false,
814
- publicationOnly = false,
815
- instance = 'singleton',
816
- } = {}) {
817
- if (schemaOnly && publicationOnly) {
818
- throw new Error('schemaOnly and publicationOnly are mutually exclusive')
819
- }
820
- // the instance rides the connection string so the pg shim's backendFor
821
- // resolves THIS namespace's DO fetch (see cloudflarePgVirtualModule).
822
- const pool = new Pool({
823
- connectionString: 'orez-do://postgres?instance=' + encodeURIComponent(instance),
824
- })
825
- const client = await pool.connect()
826
- const onCloudflareDO =
827
- typeof (globalThis.${sqlFetchGlobal} || {})[instance] ===
828
- 'function'
829
- try {
830
- // on Cloudflare every client.query() pays a libpg-query WASM parse
831
- // (~1.4s/statement) to translate PG->SQLite. so on CF we SKIP the drizzle
832
- // migration-tracking table + the no-op migration loop entirely (the
833
- // persisted SCHEMA_VERSION guard in ZeroCacheDO.migrateOnly already makes the
834
- // whole migration run once per schema) and rely on applyInitSqlDDL's single
835
- // pre-translated /batch. off-CF (node), keep the full drizzle tracking path.
836
- if (!onCloudflareDO) {
837
- await client.query(
838
- 'CREATE TABLE IF NOT EXISTS ' +
839
- quoteIdentifier(migrationTable) +
840
- ' (id TEXT PRIMARY KEY, appliedAt BIGINT NOT NULL)',
841
- )
842
- for (const migration of migrations) {
843
- const seen = await client.query(
844
- 'SELECT 1 FROM ' + quoteIdentifier(migrationTable) + ' WHERE id = $1',
845
- [migration.id],
846
- )
847
- if (seen.rows.length) continue
848
- await client.query('BEGIN')
849
- try {
850
- await migration.up(client)
851
- await client.query(
852
- 'INSERT INTO ' +
853
- quoteIdentifier(migrationTable) +
854
- ' (id, appliedAt) VALUES ($1, $2)',
855
- [migration.id, Date.now()],
856
- )
857
- await client.query('COMMIT')
858
- } catch (err) {
859
- try {
860
- await client.query('ROLLBACK')
861
- } catch {}
862
- throw err
863
- }
864
- }
865
- }
866
-
867
- // ZeroCacheDO persists SCHEMA_VERSION and calls the schema path only when
868
- // that content hash changes. Embed boots still need to verify publication
869
- // membership, but must not replay the pre-translated schema batch: its
870
- // _orez_pg_metadata INSERT OR REPLACE statements write every schema row
871
- // even when the DDL itself is idempotent. Replaying that batch on every
872
- // reconnect produced the 2026-07-10 rows-written spike.
873
- if (!publicationOnly) await applyInitSqlDDL(client, instance)
874
- // schemaOnly: apply the table DDL but DEFER ensurePublication. on CF the
875
- // publication setup makes zero-cache/the pg-proxy register every published
876
- // table's schema (~6 queries x ~42 tables = ~260 libpg parses) — that parse
877
- // burst is what tips the app/cache-DO isolate over 128 MiB during the gate
878
- // (bootstrap/get-session). those writes only need the TABLES to exist, not the
879
- // publication; the publication is for /sync replication, so we run it lazily
880
- // in ensureReady (the /sync path) where the full embed already runs. off-CF,
881
- // node has no parse cost so callers don't pass schemaOnly.
882
- if (schemaOnly) return { tables: tableNamesFromSchema(), schemaOnly: true }
883
- const tables = await ensurePublication(client, publications)
884
- return { tables: tables || [] }
885
- } finally {
886
- client.release()
887
- }
888
- }
889
- `;
890
- }
891
- //# sourceMappingURL=migration.js.map