@cavelang/store 0.34.0 → 0.36.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 (73) hide show
  1. package/README.md +884 -15
  2. package/dist/src/backup.d.ts.map +1 -1
  3. package/dist/src/backup.js +143 -70
  4. package/dist/src/backup.js.map +1 -1
  5. package/dist/src/capture-claim.d.ts +4 -0
  6. package/dist/src/capture-claim.d.ts.map +1 -0
  7. package/dist/src/capture-claim.js +26 -0
  8. package/dist/src/capture-claim.js.map +1 -0
  9. package/dist/src/descriptor.d.ts +3 -0
  10. package/dist/src/descriptor.d.ts.map +1 -0
  11. package/dist/src/descriptor.js +21 -0
  12. package/dist/src/descriptor.js.map +1 -0
  13. package/dist/src/error-message.d.ts +3 -0
  14. package/dist/src/error-message.d.ts.map +1 -0
  15. package/dist/src/error-message.js +10 -0
  16. package/dist/src/error-message.js.map +1 -0
  17. package/dist/src/index.d.ts +1 -1
  18. package/dist/src/index.d.ts.map +1 -1
  19. package/dist/src/locate.d.ts +13 -1
  20. package/dist/src/locate.d.ts.map +1 -1
  21. package/dist/src/locate.js +35 -13
  22. package/dist/src/locate.js.map +1 -1
  23. package/dist/src/node-adapter.d.ts.map +1 -1
  24. package/dist/src/node-adapter.js +29 -1
  25. package/dist/src/node-adapter.js.map +1 -1
  26. package/dist/src/provenance.d.ts +4 -0
  27. package/dist/src/provenance.d.ts.map +1 -1
  28. package/dist/src/provenance.js +36 -1
  29. package/dist/src/provenance.js.map +1 -1
  30. package/dist/src/query-sql.d.ts.map +1 -1
  31. package/dist/src/query-sql.js +4 -2
  32. package/dist/src/query-sql.js.map +1 -1
  33. package/dist/src/read-snapshot.d.ts +4 -0
  34. package/dist/src/read-snapshot.d.ts.map +1 -0
  35. package/dist/src/read-snapshot.js +22 -0
  36. package/dist/src/read-snapshot.js.map +1 -0
  37. package/dist/src/record.d.ts.map +1 -1
  38. package/dist/src/record.js +102 -25
  39. package/dist/src/record.js.map +1 -1
  40. package/dist/src/resolve.d.ts.map +1 -1
  41. package/dist/src/resolve.js +42 -18
  42. package/dist/src/resolve.js.map +1 -1
  43. package/dist/src/row.d.ts.map +1 -1
  44. package/dist/src/row.js +45 -17
  45. package/dist/src/row.js.map +1 -1
  46. package/dist/src/runtime.d.ts.map +1 -1
  47. package/dist/src/runtime.js +8 -4
  48. package/dist/src/runtime.js.map +1 -1
  49. package/dist/src/schema.d.ts +1 -2
  50. package/dist/src/schema.d.ts.map +1 -1
  51. package/dist/src/schema.js +116 -15
  52. package/dist/src/schema.js.map +1 -1
  53. package/dist/src/store.d.ts +17 -5
  54. package/dist/src/store.d.ts.map +1 -1
  55. package/dist/src/store.js +451 -104
  56. package/dist/src/store.js.map +1 -1
  57. package/package.json +4 -4
  58. package/src/backup.ts +141 -65
  59. package/src/capture-claim.ts +30 -0
  60. package/src/descriptor.ts +19 -0
  61. package/src/error-message.ts +8 -0
  62. package/src/index.ts +1 -1
  63. package/src/locate.ts +45 -13
  64. package/src/node-adapter.ts +28 -1
  65. package/src/provenance.ts +33 -1
  66. package/src/query-sql.ts +4 -2
  67. package/src/read-snapshot.ts +21 -0
  68. package/src/record.ts +102 -26
  69. package/src/resolve.ts +43 -20
  70. package/src/row.ts +29 -2
  71. package/src/runtime.ts +8 -4
  72. package/src/schema.ts +117 -18
  73. package/src/store.ts +452 -125
package/src/schema.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /** Ordered, transactional storage schema migrations (spec §13). */
2
2
 
3
- import type { Capabilities, Database } from './adapter.ts'
3
+ import type { Capabilities, Database, Row } from './adapter.ts'
4
4
  import * as Provenance from './provenance.ts'
5
+ import { errorMessage } from './error-message.ts'
5
6
 
6
- export const currentVersion = 1
7
+ export const currentVersion = 2
7
8
 
8
9
  const ddlBeforeFts = `
9
10
  CREATE TABLE IF NOT EXISTS cave_claim (
@@ -88,7 +89,8 @@ CREATE VIRTUAL TABLE IF NOT EXISTS cave_fts USING ${fullText}(
88
89
  `
89
90
 
90
91
  /** Default Node/FTS5 schema SQL retained for callers that inspect the DDL. */
91
- export const ddl = ddlBeforeFts + ftsDdl('fts5')
92
+ const transactionIndexDdl = 'CREATE INDEX IF NOT EXISTS idx_cave_tx ON cave_claim (tx);'
93
+ export const ddl = ddlBeforeFts + ftsDdl('fts5') + transactionIndexDdl
92
94
 
93
95
  const ddlFor = (capabilities: Capabilities): string =>
94
96
  ddlBeforeFts + ftsDdl(capabilities.fullText)
@@ -105,7 +107,8 @@ const migrations: readonly Migration[] = [
105
107
  db.exec(ddlFor(capabilities))
106
108
  Provenance.backfill(db)
107
109
  }
108
- }
110
+ },
111
+ { version: 2, up: db => { db.exec(transactionIndexDdl) } }
109
112
  ]
110
113
 
111
114
  export const versionOf = (db: Database): number =>
@@ -116,10 +119,21 @@ const requiredTables = [
116
119
  ] as const
117
120
 
118
121
  const requiredIndexes = [
119
- 'idx_cave_claim_key_tx', 'idx_cave_subject', 'idx_cave_verb', 'idx_cave_object',
120
- 'idx_cave_attribute', 'idx_cave_conf', 'idx_cave_context', 'idx_cave_context_claim',
121
- 'idx_cave_provenance_lookup', 'idx_cave_tag_key', 'idx_cave_tag_claim',
122
- 'idx_cave_edge_parent', 'idx_cave_edge_child', 'idx_cave_edge_role'
122
+ ['idx_cave_claim_key_tx', 'cave_claim', ['claim_key', 'tx']],
123
+ ['idx_cave_subject', 'cave_claim', ['subject']],
124
+ ['idx_cave_verb', 'cave_claim', ['verb']],
125
+ ['idx_cave_object', 'cave_claim', ['object']],
126
+ ['idx_cave_attribute', 'cave_claim', ['attribute']],
127
+ ['idx_cave_conf', 'cave_claim', ['conf']],
128
+ ['idx_cave_context', 'cave_context', ['context']],
129
+ ['idx_cave_context_claim', 'cave_context', ['claim_id', 'context']],
130
+ ['idx_cave_provenance_lookup', 'cave_provenance', ['dimension', 'value', 'claim_id']],
131
+ ['idx_cave_tag_key', 'cave_tag', ['key', 'value']],
132
+ ['idx_cave_tag_claim', 'cave_tag', ['claim_id', 'key', 'value']],
133
+ ['idx_cave_edge_parent', 'cave_edge', ['parent_id']],
134
+ ['idx_cave_edge_child', 'cave_edge', ['child_id']],
135
+ ['idx_cave_edge_role', 'cave_edge', ['role']],
136
+ ['idx_cave_tx', 'cave_claim', ['tx']]
123
137
  ] as const
124
138
 
125
139
  const requiredColumns: Readonly<Record<string, readonly string[]>> = {
@@ -135,27 +149,111 @@ const requiredColumns: Readonly<Record<string, readonly string[]>> = {
135
149
  cave_fts: ['claim_id', 'subject', 'verb', 'object', 'attribute', 'value_text', 'comment', 'raw_line']
136
150
  }
137
151
 
152
+ const requiredPrimaryKeys: Readonly<Record<string, readonly string[]>> = {
153
+ cave_claim: ['id'],
154
+ cave_provenance: ['claim_id', 'dimension', 'value']
155
+ }
156
+
157
+ const numericClaimColumns = new Set([
158
+ 'negated', 'value_num', 'value_approx', 'delta_num', 'sigma_level', 'conf', 'importance'
159
+ ])
160
+ const fractionalClaimColumns = new Set(['value_num', 'delta_num', 'sigma_level', 'conf'])
161
+
162
+ // SQLite checks INT before CHAR/CLOB/TEXT: CHARINT is integer-affinity.
163
+ const affinityFamily = (type: unknown): 'TEXT' | 'numeric' | 'BLOB' => {
164
+ const declared = String(type).toUpperCase()
165
+ if (declared.includes('INT')) return 'numeric'
166
+ if (/CHAR|CLOB|TEXT/.test(declared)) return 'TEXT'
167
+ if (declared === '' || declared.includes('BLOB')) return 'BLOB'
168
+ // INTEGER, REAL and NUMERIC all retain numeric comparison semantics.
169
+ return 'numeric'
170
+ }
171
+
138
172
  export const validate = (db: Database, version: number, schema = 'main'): void => {
139
173
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(schema)) {
140
174
  throw new Error(`CAVE: invalid SQLite schema name ${JSON.stringify(schema)}`)
141
175
  }
176
+ const indexes = requiredIndexes.filter(([name]) => version >= 2 || name !== 'idx_cave_tx')
177
+ const tables = new Map(db.prepare(`PRAGMA ${schema}.table_list`).all().map(row => [row.name, row]))
178
+ const indexLists = new Map<string, Row[]>()
179
+ const indexesOf = (table: string): Row[] => {
180
+ let rows = indexLists.get(table)
181
+ if (rows === undefined) {
182
+ rows = db.prepare(`PRAGMA ${schema}.index_list(${table})`).all()
183
+ indexLists.set(table, rows)
184
+ }
185
+ return rows
186
+ }
142
187
  const objects = new Map(
143
188
  (db.prepare(`SELECT name, type FROM ${schema}.sqlite_schema WHERE name LIKE 'cave_%' OR name LIKE 'idx_cave_%'`)
144
189
  .all() as { name: string, type: string }[]).map(row => [row.name, row.type])
145
190
  )
146
191
  const problems = [
147
- ...requiredTables.flatMap(name => objects.has(name) ? [] : [`missing table ${name}`]),
148
- ...requiredIndexes.flatMap(name => objects.get(name) === 'index' ? [] : [`missing index ${name}`]),
192
+ ...requiredTables.flatMap(name => objects.get(name) === 'table' ? [] : [`missing table ${name}`]),
193
+ ...indexes
194
+ .flatMap(([name]) => objects.get(name) === 'index' ? [] : [`missing index ${name}`]),
149
195
  ...Object.entries(requiredColumns).flatMap(([table, required]) => {
150
196
  if (!objects.has(table)) {
151
197
  return []
152
198
  }
153
- const columns = new Set(
154
- (db.prepare(`PRAGMA ${schema}.table_info(${table})`).all() as { name: string }[]).map(row => row.name)
155
- )
156
- return required.flatMap(column => columns.has(column) ? [] : [`missing column ${table}.${column}`])
199
+ const info = db.prepare(`PRAGMA ${schema}.table_info(${table})`).all()
200
+ const columns = new Set(info.map(row => row.name))
201
+ const expectedKey = requiredPrimaryKeys[table]
202
+ const key = info.filter(row => Number(row.pk) > 0)
203
+ .sort((left, right) => Number(left.pk) - Number(right.pk)).map(row => row.name)
204
+ let binaryKey = true
205
+ if (expectedKey !== undefined) {
206
+ const primary = indexesOf(table).find(row => row.origin === 'pk')
207
+ const name = typeof primary?.name === 'string' ? primary.name : undefined
208
+ const indexed = name === undefined ? [] :
209
+ db.prepare(`PRAGMA ${schema}.index_xinfo("${name.replaceAll('"', '""')}")`).all()
210
+ .filter(row => Number(row.key) === 1)
211
+ binaryKey = indexed.length === expectedKey.length &&
212
+ indexed.every(row => String(row.coll).toUpperCase() === 'BINARY')
213
+ }
214
+ return [
215
+ ...required.flatMap(column => columns.has(column) ? [] : [`missing column ${table}.${column}`]),
216
+ ...info.flatMap(row => {
217
+ const column = String(row.name)
218
+ // FTS virtual columns have module-defined storage, not ordinary affinity.
219
+ if (table === 'cave_fts' || !required.includes(column)) return []
220
+ const strict = Number(tables.get(table)?.strict) === 1
221
+ const declared = String(row.type).toUpperCase()
222
+ if (strict && table === 'cave_claim' && fractionalClaimColumns.has(column) && declared !== 'REAL') {
223
+ return [`incompatible column ${table}.${column}: expected REAL in a STRICT table`]
224
+ }
225
+ const expected = table === 'cave_claim' && numericClaimColumns.has(column) ? 'numeric' : 'TEXT'
226
+ const affinity = strict && declared === 'ANY' ? 'BLOB' : affinityFamily(row.type)
227
+ return affinity === expected ? [] :
228
+ [`incompatible column ${table}.${column}: expected ${expected} affinity`]
229
+ }),
230
+ ...(expectedKey !== undefined && (!binaryKey || key.length !== expectedKey.length || key.some((name, index) => name !== expectedKey[index]))
231
+ ? [`incompatible table ${table}: expected primary key (${expectedKey.join(', ')}) with BINARY collation`] : [])
232
+ ]
157
233
  })
158
234
  ]
235
+ if (objects.get('cave_fts') === 'table') {
236
+ const searchTable = tables.get('cave_fts')
237
+ if (searchTable?.type !== 'virtual') {
238
+ problems.push('incompatible table cave_fts: expected a virtual table for full-text search')
239
+ } else {
240
+ const searchColumns = db.prepare(`PRAGMA ${schema}.table_xinfo(cave_fts)`).all()
241
+ if (!searchColumns.some(row => row.name === 'cave_fts' && Number(row.hidden) === 1)) {
242
+ problems.push('incompatible table cave_fts: missing the hidden full-text search column')
243
+ }
244
+ }
245
+ }
246
+ for (const [name, table, expected] of indexes) {
247
+ if (objects.get(name) !== 'index') continue
248
+ const columns = db.prepare(`PRAGMA ${schema}.index_xinfo(${name})`).all()
249
+ .filter(row => Number(row.key) === 1)
250
+ const index = indexesOf(table).find(row => row.name === name)
251
+ if (index === undefined || Number(index.unique) !== 0 || Number(index.partial) !== 0 ||
252
+ columns.length !== expected.length || columns.some((column, position) =>
253
+ column.name !== expected[position] || String(column.coll).toUpperCase() !== 'BINARY')) {
254
+ problems.push(`incompatible index ${name}: expected non-unique ${table}(${expected.map(column => `${column} COLLATE BINARY`).join(', ')}) without a partial predicate`)
255
+ }
256
+ }
159
257
  if (problems.length > 0) {
160
258
  throw new Error(`CAVE: schema version ${version} is incompatible: ${problems.join(', ')}`)
161
259
  }
@@ -214,11 +312,12 @@ export const init = (
214
312
  } catch (error) {
215
313
  try {
216
314
  db.exec('ROLLBACK')
217
- } catch {
218
- // SQLite may already have rolled back a failed statement.
315
+ } catch (rollbackError) {
316
+ throw new AggregateError([error, rollbackError],
317
+ `CAVE: schema migration ${version} -> ${migration.version} failed: ${errorMessage(error)}; rollback also failed: ${errorMessage(rollbackError)}`,
318
+ { cause: error })
219
319
  }
220
- const detail = error instanceof Error ? error.message : String(error)
221
- throw new Error(`CAVE: schema migration ${version} -> ${migration.version} failed: ${detail}`)
320
+ throw new Error(`CAVE: schema migration ${version} -> ${migration.version} failed: ${errorMessage(error)}`, { cause: error })
222
321
  }
223
322
  }
224
323
  validate(db, version)