@quereus/quereus 4.13.1 → 4.14.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 (79) hide show
  1. package/dist/src/func/builtins/explain.js +1 -1
  2. package/dist/src/func/builtins/explain.js.map +1 -1
  3. package/dist/src/planner/mutation/capture-correlation.d.ts +57 -0
  4. package/dist/src/planner/mutation/capture-correlation.d.ts.map +1 -0
  5. package/dist/src/planner/mutation/capture-correlation.js +57 -0
  6. package/dist/src/planner/mutation/capture-correlation.js.map +1 -0
  7. package/dist/src/planner/mutation/decomposition.d.ts.map +1 -1
  8. package/dist/src/planner/mutation/decomposition.js +86 -23
  9. package/dist/src/planner/mutation/decomposition.js.map +1 -1
  10. package/dist/src/planner/mutation/lens-enforcement.d.ts.map +1 -1
  11. package/dist/src/planner/mutation/lens-enforcement.js +15 -20
  12. package/dist/src/planner/mutation/lens-enforcement.js.map +1 -1
  13. package/dist/src/planner/mutation/multi-source.d.ts +52 -11
  14. package/dist/src/planner/mutation/multi-source.d.ts.map +1 -1
  15. package/dist/src/planner/mutation/multi-source.js +273 -86
  16. package/dist/src/planner/mutation/multi-source.js.map +1 -1
  17. package/dist/src/planner/mutation/set-op.d.ts.map +1 -1
  18. package/dist/src/planner/mutation/set-op.js +4 -12
  19. package/dist/src/planner/mutation/set-op.js.map +1 -1
  20. package/dist/src/planner/mutation/single-source.js +1 -1
  21. package/dist/src/planner/mutation/single-source.js.map +1 -1
  22. package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
  23. package/dist/src/runtime/emit/alter-table.js +8 -10
  24. package/dist/src/runtime/emit/alter-table.js.map +1 -1
  25. package/dist/src/runtime/emit/materialized-view-helpers.d.ts +16 -4
  26. package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -1
  27. package/dist/src/runtime/emit/materialized-view-helpers.js +182 -25
  28. package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -1
  29. package/dist/src/runtime/emit/materialized-view.d.ts.map +1 -1
  30. package/dist/src/runtime/emit/materialized-view.js +1 -1
  31. package/dist/src/runtime/emit/materialized-view.js.map +1 -1
  32. package/dist/src/schema/ddl-generator.d.ts.map +1 -1
  33. package/dist/src/schema/ddl-generator.js +65 -24
  34. package/dist/src/schema/ddl-generator.js.map +1 -1
  35. package/dist/src/schema/lens-ack.d.ts +1 -1
  36. package/dist/src/schema/lens-ack.js +2 -2
  37. package/dist/src/schema/lens-compiler.d.ts.map +1 -1
  38. package/dist/src/schema/lens-compiler.js +63 -34
  39. package/dist/src/schema/lens-compiler.js.map +1 -1
  40. package/dist/src/schema/lens-prover.d.ts +4 -4
  41. package/dist/src/schema/lens-prover.d.ts.map +1 -1
  42. package/dist/src/schema/lens-prover.js +13 -5
  43. package/dist/src/schema/lens-prover.js.map +1 -1
  44. package/dist/src/schema/lens.d.ts +1 -1
  45. package/dist/src/schema/manager.d.ts.map +1 -1
  46. package/dist/src/schema/manager.js +25 -13
  47. package/dist/src/schema/manager.js.map +1 -1
  48. package/dist/src/schema/reserved-tags.js +1 -1
  49. package/dist/src/schema/reserved-tags.js.map +1 -1
  50. package/dist/src/schema/schema-differ.d.ts.map +1 -1
  51. package/dist/src/schema/schema-differ.js +4 -3
  52. package/dist/src/schema/schema-differ.js.map +1 -1
  53. package/dist/src/schema/table.d.ts +41 -34
  54. package/dist/src/schema/table.d.ts.map +1 -1
  55. package/dist/src/schema/table.js +24 -43
  56. package/dist/src/schema/table.js.map +1 -1
  57. package/dist/src/vtab/memory/layer/alter-column.d.ts +8 -1
  58. package/dist/src/vtab/memory/layer/alter-column.d.ts.map +1 -1
  59. package/dist/src/vtab/memory/layer/alter-column.js +20 -10
  60. package/dist/src/vtab/memory/layer/alter-column.js.map +1 -1
  61. package/dist/src/vtab/memory/layer/base.d.ts +9 -1
  62. package/dist/src/vtab/memory/layer/base.d.ts.map +1 -1
  63. package/dist/src/vtab/memory/layer/base.js +12 -1
  64. package/dist/src/vtab/memory/layer/base.js.map +1 -1
  65. package/dist/src/vtab/memory/layer/manager.d.ts +23 -18
  66. package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
  67. package/dist/src/vtab/memory/layer/manager.js +44 -33
  68. package/dist/src/vtab/memory/layer/manager.js.map +1 -1
  69. package/dist/src/vtab/memory/layer/row-convert.d.ts +13 -0
  70. package/dist/src/vtab/memory/layer/row-convert.d.ts.map +1 -1
  71. package/dist/src/vtab/memory/layer/row-convert.js +24 -0
  72. package/dist/src/vtab/memory/layer/row-convert.js.map +1 -1
  73. package/dist/src/vtab/memory/layer/transaction.d.ts +21 -7
  74. package/dist/src/vtab/memory/layer/transaction.d.ts.map +1 -1
  75. package/dist/src/vtab/memory/layer/transaction.js +30 -14
  76. package/dist/src/vtab/memory/layer/transaction.js.map +1 -1
  77. package/dist/src/vtab/module.d.ts +20 -2
  78. package/dist/src/vtab/module.d.ts.map +1 -1
  79. package/package.json +3 -3
@@ -41,9 +41,9 @@ export function materializedViewNotASetError(schemaName, viewName) {
41
41
  * the schema declares NOT NULL *and* that is a physical-PK member. This is the exact
42
42
  * reachable contradiction: the view's `order by <col>` seeded the column into the
43
43
  * backing's **physical** primary key ({@link computeBackingPrimaryKey}), which keeps it
44
- * declared NOT NULL even after the source column loosened to nullable — a physical-PK
45
- * column cannot drop NOT NULL (`MemoryTableManager.alterColumn` refuses it, and the
46
- * refresh reshape masks the doomed loosen — see {@link isPhysicalPkColumn}). Once the
44
+ * declared NOT NULL even after the source column loosened to nullable — the refresh reshape
45
+ * masks the loosening of a physical-PK column rather than emitting it (see
46
+ * {@link isPhysicalPkColumn}). Once the
47
47
  * source column becomes nullable and the recomputed body yields a NULL row, storing it
48
48
  * would leave the backing schema declaring NOT NULL while holding a NULL — a silent
49
49
  * contradiction. Both maintenance vectors raise this instead, naming the column, the cause,
@@ -73,11 +73,20 @@ export function nullInNotNullSeededPkError(schemaName, viewName, columnName) {
73
73
  * `keysOf` (all-columns fallback when none — such an MV is incremental-ineligible
74
74
  * until Phase 2). Re-planning here is cheap relative to materialization and keeps
75
75
  * the create/refresh emitters free of optimizer plumbing.
76
+ *
77
+ * `backing` names the module that hosts (or would host) this shape's backing table,
78
+ * plus (for a comparison) the table the result will be compared against. Supplying it
79
+ * runs the derived shape through that module's own create-time normalization
80
+ * ({@link normalizeBackingShape}) so every downstream comparison sees the shape the
81
+ * module would actually create — see that function for why, and for what `against`
82
+ * resolves. Omit only where no backing identity exists; every consumer that compares
83
+ * against a live backing must pass it.
76
84
  */
77
- export function deriveBackingShape(db, schemaName, bodySql, explicitColumns) {
85
+ export function deriveBackingShape(db, schemaName, bodySql, explicitColumns, backing) {
78
86
  // Suppress the read-side rewrite: we are computing the MV body to derive/populate
79
87
  // its OWN backing, so it must not be rewritten to read that backing.
80
- return db.schemaManager.withSuppressedMaterializedViewRewrite(() => deriveBackingShapeUnguarded(db, schemaName, bodySql, explicitColumns));
88
+ const shape = db.schemaManager.withSuppressedMaterializedViewRewrite(() => deriveBackingShapeUnguarded(db, schemaName, bodySql, explicitColumns));
89
+ return normalizeBackingShape(db, schemaName, shape, backing?.moduleName, backing?.moduleArgs, backing?.against);
81
90
  }
82
91
  function deriveBackingShapeUnguarded(db, schemaName, bodySql, explicitColumns) {
83
92
  // Home-schema path: the body's unqualified source names resolve next to the
@@ -278,6 +287,146 @@ tags) {
278
287
  tags: tags && Object.keys(tags).length > 0 ? tags : undefined,
279
288
  };
280
289
  }
290
+ /**
291
+ * Applies the backing module's own create-time schema normalization
292
+ * ({@link VirtualTableModule.normalizeCreateSchema}) to a derived {@link BackingShape},
293
+ * so a shape derived from the body is expressed in the SAME terms as a table the module
294
+ * actually created.
295
+ *
296
+ * Why: a module may own per-column physical attributes the body's output type cannot
297
+ * predict. The store module keys an *implicit*-collation text primary-key column under
298
+ * its table-level key collation K (default NOCASE — `reconcilePkCollations`), so a
299
+ * persisted backing reads `COLLATE NOCASE` while the re-derived shape says BINARY. Every
300
+ * shape comparison ({@link backingShapeMatches}, {@link describeBackingShapeMismatch},
301
+ * {@link classifyBackingReshape}) would then report a difference that does not exist,
302
+ * refilling the backing on every reopen. Normalizing here — at the SOURCE, before any
303
+ * comparison — keeps both sides post-normalization rather than teaching each comparison
304
+ * site to special-case a module's rewrite.
305
+ *
306
+ * Identity for a module without the hook (memory, and every third-party module), and for
307
+ * an unresolvable module name — the real `no virtual table module named …` /
308
+ * capability diagnostics stay with {@link buildBackingTableSchema}, which is deliberately
309
+ * NOT reused here: it carries the `getBackingHost` gate (whose error must keep firing at
310
+ * its own site) and stamps primary-key flags onto `shape.columns`, neither of which
311
+ * belongs on a pure "what would this become?" probe.
312
+ *
313
+ * **`against` — two-valued compatibility for an implicit attribute.** The hook only ever
314
+ * rewrites attributes the shape left IMPLICIT (the store touches a text primary-key column
315
+ * only when it declares no `collate`). An implicit attribute is genuinely ambiguous once a
316
+ * table exists, because explicitness does not survive a catalog round-trip
317
+ * (`ColumnSchema.collationExplicit` is not persisted; a declared `collate binary` reloads
318
+ * indistinguishable from no clause at all). So an implicit shape column is compatible with
319
+ * a live column carrying EITHER the shape's own declared value or the module's normalized
320
+ * one — both are physically consistent with the body. When the caller names the table this
321
+ * shape will be compared against, the rewrite is therefore kept only where the RAW shape
322
+ * does not already agree with that table; a table-form maintained table whose text key was
323
+ * declared `collate binary` keeps matching its own declaration, while a materialized-view
324
+ * backing the module keyed under K matches that. Omit `against` on a create path (nothing
325
+ * to be compatible with yet) and the module's answer is taken verbatim.
326
+ *
327
+ * A collation the module would NOT produce and the body did NOT declare (a backing
328
+ * persisted under RTRIM, say) matches neither reading and still reports a mismatch. The
329
+ * agreement test looks only at the attributes the hook actually rewrote — see
330
+ * {@link rawReadingIsTheLiveOne}.
331
+ *
332
+ * Only `columns` can change; `primaryKey`, `ordering`, `sourceTables`, `coarsenedKey`,
333
+ * and `allProvedKeys` pass through untouched.
334
+ */
335
+ function normalizeBackingShape(db, schemaName, shape, moduleName, moduleArgs, against) {
336
+ const resolvedModuleName = normalizeBackingModuleName(moduleName);
337
+ const module = db.schemaManager.getModule(resolvedModuleName)?.module;
338
+ if (!module?.normalizeCreateSchema)
339
+ return shape;
340
+ // Copy the columns into the probe: the contract says the hook is pure, but a
341
+ // sloppy implementation mutating a column in place would otherwise corrupt the
342
+ // caller's shape. The probe uses the PHYSICAL primary key
343
+ // ({@link computeBackingPrimaryKey} — ordering-seeded), not `shape.primaryKey`,
344
+ // because that is the key the module will actually be handed at create; a text
345
+ // ORDERING column is reconciled there too and would keep diverging otherwise.
346
+ const probeColumns = shape.columns.map(c => ({ ...c }));
347
+ const probe = {
348
+ name: `<materialized-view backing shape probe>`,
349
+ schemaName,
350
+ columns: probeColumns,
351
+ columnIndexMap: buildColumnIndexMap(probeColumns),
352
+ primaryKeyDefinition: computeBackingPrimaryKey(shape).map(pk => ({
353
+ index: pk.index,
354
+ desc: pk.desc,
355
+ collation: probeColumns[pk.index]?.collation,
356
+ })),
357
+ checkConstraints: [],
358
+ vtabModule: module,
359
+ vtabModuleName: resolvedModuleName,
360
+ vtabArgs: moduleArgs ? { ...moduleArgs } : {},
361
+ isView: false,
362
+ estimatedRows: 0,
363
+ };
364
+ const normalized = module.normalizeCreateSchema(probe);
365
+ if (normalized.columns === probe.columns)
366
+ return shape; // hook declined to rewrite
367
+ // A reshape is a contract violation, not a shape difference: the hook may only
368
+ // adjust per-column attributes it owns physically. Fail loud rather than let a
369
+ // misbehaving module silently redefine the backing's column list.
370
+ if (normalized.columns.length !== shape.columns.length) {
371
+ throw new QuereusError(`module '${resolvedModuleName}' normalizeCreateSchema changed the column count `
372
+ + `(${shape.columns.length} → ${normalized.columns.length}); it may only adjust per-column attributes`, StatusCode.INTERNAL);
373
+ }
374
+ for (let i = 0; i < shape.columns.length; i++) {
375
+ if (normalized.columns[i].name !== shape.columns[i].name) {
376
+ throw new QuereusError(`module '${resolvedModuleName}' normalizeCreateSchema renamed or reordered column ${i} `
377
+ + `('${shape.columns[i].name}' → '${normalized.columns[i].name}'); it may only adjust per-column attributes`, StatusCode.INTERNAL);
378
+ }
379
+ }
380
+ // Two-valued compatibility (see the docblock): where the RAW shape column already
381
+ // agrees with the table this shape is about to be compared against, that reading is
382
+ // the live one and the module's alternative is not applied.
383
+ // NOTE: the two readings exist only because `ColumnSchema.collationExplicit` does not
384
+ // survive a catalog round-trip. If explicitness is ever persisted, the live table can
385
+ // answer "did the author declare this?" directly and this collapses to a single
386
+ // reading — take the module's answer iff the live column is implicit.
387
+ // NOTE: `against` is read POSITIONALLY, which is what the rename cascade needs (names
388
+ // have shifted there, the attributes have not). It is only ever consulted for a column
389
+ // the hook rewrote, so a pure append/drop reshape still resolves correctly — but if a
390
+ // reshape ever MOVES a rewritten column to a position the live table fills with a
391
+ // different column, this asks the wrong column and the alignment-based
392
+ // {@link classifyBackingReshape} would see a collation the module did not produce. Give
393
+ // callers a way to say "align by name" if a reshape is ever seen misclassified this way.
394
+ const columns = normalized.columns.map((col, i) => {
395
+ const raw = shape.columns[i];
396
+ if (col === raw)
397
+ return raw;
398
+ const live = against?.columns[i];
399
+ if (live && rawReadingIsTheLiveOne(raw, col, live))
400
+ return raw;
401
+ return col;
402
+ });
403
+ if (columns.every((c, i) => c === shape.columns[i]))
404
+ return shape;
405
+ return { ...shape, columns };
406
+ }
407
+ /**
408
+ * The two-valued compatibility test of {@link normalizeBackingShape}: does the RAW
409
+ * (pre-normalization) reading of a column agree with the live table on every attribute
410
+ * the hook actually rewrote?
411
+ *
412
+ * Only the REWRITTEN attributes are compared. An attribute the hook left alone carries no
413
+ * second reading, so a difference in it is a genuine shape change for the comparison sites
414
+ * to report — it says nothing about which reading of the rewritten attribute is live.
415
+ * Testing all three attributes unconditionally instead would let an unrelated difference
416
+ * decide the collation: a NOT NULL loosening on a physical-PK column — which
417
+ * {@link describeBackingShapeMismatch} deliberately masks — would flip a declared
418
+ * `collate binary` key over to the module's K and turn a data-only refresh into a
419
+ * spurious "primary-key column 0 collation BINARY → NOCASE" inexpressible reshape.
420
+ */
421
+ function rawReadingIsTheLiveOne(raw, normalized, live) {
422
+ if (!backingTypeMatches(raw, normalized) && !backingTypeMatches(raw, live))
423
+ return false;
424
+ if (!backingNotNullMatches(raw, normalized) && !backingNotNullMatches(raw, live))
425
+ return false;
426
+ if (!backingCollationMatches(raw, normalized) && !backingCollationMatches(raw, live))
427
+ return false;
428
+ return true;
429
+ }
281
430
  /** Runs the body to completion and returns its rows (raw `Row` arrays). Uses the
282
431
  * no-transaction-management primitive — the caller is already inside DDL execution. */
283
432
  export async function collectBodyRows(db, schemaName, bodySql) {
@@ -396,7 +545,7 @@ function warnKeyCoarsening(schemaName, viewName, info) {
396
545
  */
397
546
  export async function materializeView(db, def, preDerivedShape) {
398
547
  const sm = db.schemaManager;
399
- const shape = preDerivedShape ?? deriveBackingShape(db, def.schemaName, def.bodySql, def.columns);
548
+ const shape = preDerivedShape ?? deriveBackingShape(db, def.schemaName, def.bodySql, def.columns, { moduleName: def.backingModuleName, moduleArgs: def.backingModuleArgs });
400
549
  // Lives here — not in deriveBackingShape — because the refresh path reaches a
401
550
  // legitimate mismatch after a source ALTER (see the assert's docstring).
402
551
  assertDeclaredColumnArity(def, shape);
@@ -913,7 +1062,7 @@ discardBackingOnFailure = false) {
913
1062
  // renamed positionally to it and the name check skipped; otherwise natural output
914
1063
  // names with the strict declared-shape check (the body must already be aliased to
915
1064
  // the declared names — the attach verb / implicit-create posture).
916
- const shape = deriveBackingShape(db, schemaName, bodySql, positionalRename ? recordedColumns : undefined);
1065
+ const shape = deriveBackingShape(db, schemaName, bodySql, positionalRename ? recordedColumns : undefined, { moduleName: table.vtabModuleName, moduleArgs: table.vtabArgs, against: table });
917
1066
  // Explicit rename-list arity guard. `deriveBackingShape` sizes the shape to the
918
1067
  // BODY's arity (a surplus rename name is dropped, a missing one padded), so a
919
1068
  // list whose length disagrees with the body would otherwise record an
@@ -1272,7 +1421,7 @@ export async function createMaintainedTable(db, stmt) {
1272
1421
  const declared = sm.buildDeclaredTableSchema(stmt);
1273
1422
  const recordedColumns = explicit ? declared.columns.map(c => c.name) : undefined;
1274
1423
  const bodySql = astToString(stmt.maintained.select);
1275
- const shape = deriveBackingShape(db, schemaName, bodySql, explicit ? recordedColumns : undefined);
1424
+ const shape = deriveBackingShape(db, schemaName, bodySql, explicit ? recordedColumns : undefined, { moduleName: declared.vtabModuleName, moduleArgs: declared.vtabArgs, against: declared });
1276
1425
  const mismatch = describeAttachShapeMismatch(declared, shape, explicit);
1277
1426
  if (mismatch) {
1278
1427
  throw new QuereusError(`cannot create maintained table '${schemaName}.${name}': ${mismatch}`, StatusCode.ERROR, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
@@ -1358,8 +1507,8 @@ export async function rebuildBacking(db, mv, validationColumns) {
1358
1507
  // a backing column declared NOT NULL that is also a physical-PK member cannot legally
1359
1508
  // hold the recomputed NULL a nullable source now produces. The ordering-seeded physical
1360
1509
  // PK (computeBackingPrimaryKey) pins such a column NOT NULL even after its source column
1361
- // dropped NOT NULL — the memory manager refuses to loosen a PK column and the reshape
1362
- // masks the doomed loosen — so at this point the only still-NOT-NULL columns that can
1510
+ // dropped NOT NULL — the reshape masks the loosening of a physical-PK column rather than
1511
+ // emitting it — so at this point the only still-NOT-NULL columns that can
1363
1512
  // carry source-nullable data are the physical-PK ones. Storing the NULL would make the
1364
1513
  // backing schema declare NOT NULL while holding a NULL; reject it before either swap.
1365
1514
  // NOTE: narrow loud guard until `debt-mv-ordering-seed-to-materialized-index` replaces
@@ -1462,9 +1611,17 @@ function backingCollationMatches(a, b) {
1462
1611
  /** Whether `columnNameLower` (already lowercased) names a column of `table`'s
1463
1612
  * *physical* primary key ({@link TableSchema.primaryKeyDefinition} — which for an
1464
1613
  * MV backing includes any ordering-seeded columns, {@link computeBackingPrimaryKey}).
1465
- * A physical-PK column is NOT NULL by definition: `MemoryTableManager.alterColumn`
1466
- * refuses to DROP NOT NULL on one, so the refresh reshape must never try to loosen
1467
- * its NOT NULL — see the two callers below. */
1614
+ * An MV backing keeps its physical-PK columns NOT NULL, so the refresh reshape must
1615
+ * never try to loosen one — see the two callers below.
1616
+ *
1617
+ * NOTE: that is now a POLICY of the MV backing, not an engine constraint. A primary key
1618
+ * column may be nullable in general (docs/schema.md § Primary-key nullability) and
1619
+ * `alter column … drop not null` on one is accepted by both backends; the backing keeps
1620
+ * its seeded key columns NOT NULL because the ordering seed treats them as a total order
1621
+ * and `assertNoNullInNotNullSeededPk` is the loud guard for the contradiction. If
1622
+ * ordering-seeded backings ever need to carry NULLs in the seeded column, this mask and
1623
+ * that guard are the pair to revisit together — `debt-mv-ordering-seed-to-materialized-index`
1624
+ * removes the need for both. */
1468
1625
  function isPhysicalPkColumn(table, columnNameLower) {
1469
1626
  return table.primaryKeyDefinition.some(def => table.columns[def.index]?.name.toLowerCase() === columnNameLower);
1470
1627
  }
@@ -1487,8 +1644,8 @@ function describeBackingShapeMismatch(current, shape) {
1487
1644
  }
1488
1645
  if (!backingNotNullMatches(a, b)) {
1489
1646
  // A physical-PK column stays NOT NULL in the backing regardless of the
1490
- // re-derived logical nullability (a PK column cannot hold NULL — the memory
1491
- // manager refuses to DROP NOT NULL on it), so a NOT-NULL→nullable *loosening*
1647
+ // re-derived logical nullability (an MV-backing policy — see
1648
+ // {@link isPhysicalPkColumn}), so a NOT-NULL→nullable *loosening*
1492
1649
  // of a PK column is NOT a shape difference. Masking it lets refresh keep the
1493
1650
  // data-only rebuild path instead of emitting a doomed `loosenNotNull` op. Tight
1494
1651
  // on purpose: only a loosening (`current` NOT NULL, derived nullable) of a
@@ -1836,14 +1993,14 @@ export function tryRecompileMaterializedViewLive(db, mv, oldObject, newObject) {
1836
1993
  try {
1837
1994
  const d = mv.derivation;
1838
1995
  const bodySql = astToString(d.selectAst);
1839
- const shape = deriveBackingShape(db, mv.schemaName, bodySql, d.columns);
1996
+ const schema = db.schemaManager.getSchemaOrFail(mv.schemaName);
1997
+ const live = schema.getTable(mv.name);
1998
+ const backing = isMaintainedTable(live) ? live : mv;
1999
+ const shape = deriveBackingShape(db, mv.schemaName, bodySql, d.columns, { moduleName: mv.vtabModuleName, moduleArgs: mv.vtabArgs, against: backing });
1840
2000
  if (!sameSourceTables(d.sourceTables, shape.sourceTables)) {
1841
2001
  log('Marking materialized view %s.%s stale instead of recompiling: re-planned source tables (%s) disagree with the recorded set (%s) — REFRESH re-derives', mv.schemaName, mv.name, shape.sourceTables.join(', '), d.sourceTables.join(', '));
1842
2002
  return false;
1843
2003
  }
1844
- const schema = db.schemaManager.getSchemaOrFail(mv.schemaName);
1845
- const live = schema.getTable(mv.name);
1846
- const backing = isMaintainedTable(live) ? live : mv;
1847
2004
  const mismatch = describeBackingShapeMismatch(backing, shape);
1848
2005
  if (mismatch) {
1849
2006
  // Relaxed superkey gate: columns match structurally AND the existing backing
@@ -1964,10 +2121,10 @@ function classifyBackingReshape(current, shape) {
1964
2121
  if (!backingNotNullMatches(from, to)) {
1965
2122
  if (to.notNull === true)
1966
2123
  postReconcileOps.push({ kind: 'tightenNotNull', name });
1967
- // A physical-PK column cannot hold NULL, so the backing keeps it NOT NULL and
1968
- // the memory manager refuses to DROP NOT NULL on it: skip the doomed
1969
- // `loosenNotNull` op for a physical-PK column of `current` (matched by its
1970
- // pre-rename name). Reached only when reshape is entered for SOME OTHER genuine
2124
+ // An MV backing keeps its physical-PK columns NOT NULL (a backing policy — see
2125
+ // {@link isPhysicalPkColumn}), so skip the `loosenNotNull` op for a physical-PK
2126
+ // column of `current` (matched by its pre-rename name) rather than emitting a
2127
+ // loosening the backing must not take. Reached only when reshape is entered for SOME OTHER genuine
1971
2128
  // shape change coexisting with a PK-column loosening in the same refresh;
1972
2129
  // touch point #1 masks the loosening-only case before reshape. See the NOTE at
1973
2130
  // describeBackingShapeMismatch / computeBackingPrimaryKey (~line 236).
@@ -2713,7 +2870,7 @@ export async function restoreUnaffectedMaterializedViews(db, preStale) {
2713
2870
  // Throws when the body no longer plans against the renamed catalog
2714
2871
  // (e.g. a chained MV referencing a renamed-away output name) → catch
2715
2872
  // below leaves it stale.
2716
- const shape = deriveBackingShape(db, mv.schemaName, bodySql, d.columns);
2873
+ const shape = deriveBackingShape(db, mv.schemaName, bodySql, d.columns, { moduleName: mv.vtabModuleName, moduleArgs: mv.vtabArgs, against: schema.getTable(mv.name) });
2717
2874
  // The retry of a failure-marked MV must not revive an inconsistent record: a
2718
2875
  // rewrite that threw between the in-place AST mutation and the derived-field
2719
2876
  // re-key leaves the OLD derivation (un-re-keyed `sourceTables`) holding the
@@ -2769,7 +2926,7 @@ function sameSourceTables(a, b) {
2769
2926
  * rewrite (a mid-statement failure).
2770
2927
  */
2771
2928
  async function renameShiftedBackingColumns(db, schema, mv, bodySql, preDerivedShape) {
2772
- const shape = preDerivedShape ?? deriveBackingShape(db, mv.schemaName, bodySql, mv.derivation.columns);
2929
+ const shape = preDerivedShape ?? deriveBackingShape(db, mv.schemaName, bodySql, mv.derivation.columns, { moduleName: mv.vtabModuleName, moduleArgs: mv.vtabArgs, against: schema.getTable(mv.name) });
2773
2930
  const backing = schema.getTable(mv.name);
2774
2931
  if (!backing) {
2775
2932
  throw new QuereusError(`Internal error: maintained table '${mv.name}' not found during backing-column rename`, StatusCode.INTERNAL);