@quereus/store 4.4.0 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/src/common/cached-kv-store.js +4 -0
- package/dist/src/common/cached-kv-store.js.map +1 -1
- package/dist/src/common/encoding.d.ts +32 -1
- package/dist/src/common/encoding.d.ts.map +1 -1
- package/dist/src/common/encoding.js +42 -10
- package/dist/src/common/encoding.js.map +1 -1
- package/dist/src/common/index.d.ts +3 -2
- package/dist/src/common/index.d.ts.map +1 -1
- package/dist/src/common/index.js +3 -1
- package/dist/src/common/index.js.map +1 -1
- package/dist/src/common/json-key.d.ts +96 -0
- package/dist/src/common/json-key.d.ts.map +1 -0
- package/dist/src/common/json-key.js +204 -0
- package/dist/src/common/json-key.js.map +1 -0
- package/dist/src/common/key-builder.d.ts +40 -5
- package/dist/src/common/key-builder.d.ts.map +1 -1
- package/dist/src/common/key-builder.js +59 -21
- package/dist/src/common/key-builder.js.map +1 -1
- package/dist/src/common/kv-store.d.ts +12 -2
- package/dist/src/common/kv-store.d.ts.map +1 -1
- package/dist/src/common/store-module.d.ts +64 -15
- package/dist/src/common/store-module.d.ts.map +1 -1
- package/dist/src/common/store-module.js +349 -125
- package/dist/src/common/store-module.js.map +1 -1
- package/dist/src/common/store-table.d.ts +125 -11
- package/dist/src/common/store-table.d.ts.map +1 -1
- package/dist/src/common/store-table.js +235 -34
- package/dist/src/common/store-table.js.map +1 -1
- package/dist/src/testing/kv-conformance.d.ts.map +1 -1
- package/dist/src/testing/kv-conformance.js +32 -0
- package/dist/src/testing/kv-conformance.js.map +1 -1
- package/package.json +5 -5
|
@@ -12,15 +12,15 @@
|
|
|
12
12
|
* - {prefix}.__stats__ - Unified stats store (row counts for all tables)
|
|
13
13
|
* - Catalog store: __catalog__ - DDL metadata keyed by {schema}.{table}
|
|
14
14
|
*/
|
|
15
|
-
import { AccessPlanBuilder, QuereusError, StatusCode, buildColumnIndexMap, columnDefToSchema, compilePredicate, inferType, tryFoldLiteral, validateAndParse, buildAdvertisementsFromTags, resolveNamedConstraintClass, validateCollationForType, buildUniqueConstraintSchema, buildForeignKeyConstraintSchema, buildCheckConstraintSchema, validateForeignKeyOverExistingRows,
|
|
15
|
+
import { AccessPlanBuilder, QuereusError, StatusCode, buildColumnIndexMap, columnDefToSchema, compilePredicate, inferType, tryFoldLiteral, validateAndParse, buildAdvertisementsFromTags, resolveNamedConstraintClass, validateCollationForType, buildUniqueConstraintSchema, buildForeignKeyConstraintSchema, buildCheckConstraintSchema, validateForeignKeyOverExistingRows, appendIndexToTableSchema, logicalTypeCanHoldText, serializeKey, isMaintainedTable, renameColumnInIndexPredicates, renameTableInIndexPredicates, renameColumnInCheckConstraints, renameTableInCheckConstraints } from '@quereus/quereus';
|
|
16
16
|
import { TransactionCoordinator } from './transaction.js';
|
|
17
17
|
import { StoreConnection } from './store-connection.js';
|
|
18
18
|
import { StoreBackingHost } from './backing-host.js';
|
|
19
|
-
import { StoreTable, resolvePkKeyCollations, columnCanHoldText, keyOrderMatchesCollation, pkOrderPreservingPrefixLength, withImplicitUniqueIndexes, implicitUniqueIndexName } from './store-table.js';
|
|
19
|
+
import { StoreTable, resolvePkKeyCollations, resolvePkKeyTransforms, resolveIndexKeyTransforms, storeSemanticKeyTransform, columnCanHoldText, keyOrderMatchesCollation, pkOrderPreservingPrefixLength, withImplicitUniqueIndexes, implicitUniqueIndexName } from './store-table.js';
|
|
20
20
|
import { buildCatalogKey, buildCatalogScanBounds, buildDataKey, buildDataStoreName, buildIndexKey, buildIndexStoreName, buildFullScanBounds, buildStatsKey, buildViewCatalogKey, buildMaterializedViewCatalogKey, parseMaterializedViewCatalogKey, buildMetaCatalogKey, CLEAN_SHUTDOWN_META_NAME, STALE_MVS_META_NAME, classifyCatalogKey, } from './key-builder.js';
|
|
21
21
|
import { assertNoUnpairedSurrogate } from './encoding.js';
|
|
22
22
|
import { deserializeRow } from './serialization.js';
|
|
23
|
-
import { generateTableDDL, generateIndexDDL, generateViewDDL, generateMaintainedTableDDL, generateIndexTagsDDL, isHiddenImplicitIndex, exposedImplicitIndexes } from '@quereus/quereus';
|
|
23
|
+
import { generateTableDDL, generateIndexDDL, generateDropTableDDL, generateDropIndexDDL, generateViewDDL, generateMaintainedTableDDL, generateIndexTagsDDL, isHiddenImplicitIndex, exposedImplicitIndexes } from '@quereus/quereus';
|
|
24
24
|
/**
|
|
25
25
|
* Default serialized-byte budget for a single index-build write batch. Bounds
|
|
26
26
|
* heap directly: {@link StoreModule.buildIndexEntries} flushes and starts a fresh
|
|
@@ -113,6 +113,42 @@ function rangeRoles(colIdx) {
|
|
|
113
113
|
function equalityRoles(colIdxs) {
|
|
114
114
|
return colIdxs.map(colIdx => ({ colIdx, ops: EQ_OPS }));
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Guard the FULL persisted schema text, not just the object's own name: a lone
|
|
118
|
+
* surrogate anywhere in it — a quoted column name, a `default '…'` string literal, a
|
|
119
|
+
* `check` constraint's string constant — folds to U+FFFD under `TextEncoder` and would
|
|
120
|
+
* read back as different schema text than what was created. Independent of the
|
|
121
|
+
* catalog-key builders' identifier guard; it catches what they cannot see.
|
|
122
|
+
*/
|
|
123
|
+
function assertPersistableDdlText(ddl) {
|
|
124
|
+
assertNoUnpairedSurrogate(ddl, 'persisted schema text');
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The catalog entry a plain view would be persisted as. Building it is the check:
|
|
128
|
+
* {@link buildViewCatalogKey} rejects an unencodable name, and the caller then runs
|
|
129
|
+
* {@link assertPersistableDdlText} over `ddl`. Shared by the write path
|
|
130
|
+
* ({@link StoreModule.saveViewDDL}) and the pre-flight veto
|
|
131
|
+
* ({@link StoreModule.assertCatalogObjectPersistable}) so the two cannot drift.
|
|
132
|
+
*/
|
|
133
|
+
function viewCatalogEntry(view) {
|
|
134
|
+
return {
|
|
135
|
+
key: buildViewCatalogKey(view.schemaName, view.name),
|
|
136
|
+
ddl: generateViewDDL(view),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The catalog entry a materialized view would be persisted as, or `undefined` when
|
|
141
|
+
* `table` carries no derivation (not an MV — nothing lands under the MV prefix).
|
|
142
|
+
* Counterpart of {@link viewCatalogEntry}; same write/veto sharing.
|
|
143
|
+
*/
|
|
144
|
+
function maintainedViewCatalogEntry(table) {
|
|
145
|
+
if (!isMaintainedTable(table))
|
|
146
|
+
return undefined;
|
|
147
|
+
return {
|
|
148
|
+
key: buildMaterializedViewCatalogKey(table.schemaName, table.name),
|
|
149
|
+
ddl: generateMaintainedTableDDL(table),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
116
152
|
/**
|
|
117
153
|
* Generic store module that works with any KVStoreProvider.
|
|
118
154
|
*
|
|
@@ -372,6 +408,14 @@ export class StoreModule {
|
|
|
372
408
|
* stores in the set therefore causes no false rejects and keeps the
|
|
373
409
|
* reject-before-any-side-effect guarantee uniform.
|
|
374
410
|
*/
|
|
411
|
+
// NOTE: the builders below carry the unpaired-surrogate identifier guard, so this
|
|
412
|
+
// enumeration can THROW on an EXISTING schema object, not just on the incoming one —
|
|
413
|
+
// an unrelated CREATE/RENAME in the same schema would then fail with a confusing
|
|
414
|
+
// "cannot store the identifier ..." error. Unreachable today: `create` throws before
|
|
415
|
+
// the table is registered, and a catalog entry written before the guard existed was
|
|
416
|
+
// folded to a real U+FFFD character, which passes. If a lone-surrogate-named table ever
|
|
417
|
+
// does reach the schema manager by another route, skip such an object here (it occupies
|
|
418
|
+
// no store) rather than letting the enumeration throw.
|
|
375
419
|
collectOccupiedStoreNames(db, schemaName) {
|
|
376
420
|
const names = new Map();
|
|
377
421
|
const add = (s) => {
|
|
@@ -561,12 +605,15 @@ export class StoreModule {
|
|
|
561
605
|
// torn down too — the engine-facing `.indexes` omits them and would leak them.
|
|
562
606
|
const indexNames = this.materializedIndexNames(table, currentSchema);
|
|
563
607
|
await this.tearDownTableStorage(schemaName, tableName, indexNames);
|
|
564
|
-
// Emit schema change event for table drop
|
|
608
|
+
// Emit schema change event for table drop. The `ddl` is what a sync peer
|
|
609
|
+
// re-executes to replicate the drop — a drop with no DDL crosses the wire as
|
|
610
|
+
// an empty statement and silently changes nothing on the receiver.
|
|
565
611
|
this.eventEmitter?.emitSchemaChange({
|
|
566
612
|
type: 'drop',
|
|
567
613
|
objectType: 'table',
|
|
568
614
|
schemaName,
|
|
569
615
|
objectName: tableName,
|
|
616
|
+
ddl: generateDropTableDDL(schemaName, tableName),
|
|
570
617
|
});
|
|
571
618
|
}
|
|
572
619
|
/**
|
|
@@ -745,12 +792,17 @@ export class StoreModule {
|
|
|
745
792
|
// skips (identical) — see persistCatalogIfChanged.
|
|
746
793
|
await this.saveTableDDL(updatedSchema);
|
|
747
794
|
table.markDdlSaved();
|
|
748
|
-
// Emit schema change event
|
|
795
|
+
// Emit schema change event. `updatedSchema` (not the pre-create
|
|
796
|
+
// `tableSchema`) is the owner the canonical CREATE INDEX renders against —
|
|
797
|
+
// it is only read for the table's qualified name and column names, but
|
|
798
|
+
// passing the post-create schema keeps the rendering consistent with what a
|
|
799
|
+
// receiving peer regenerates when it compares definitions.
|
|
749
800
|
this.eventEmitter?.emitSchemaChange({
|
|
750
801
|
type: 'create',
|
|
751
802
|
objectType: 'index',
|
|
752
803
|
schemaName,
|
|
753
804
|
objectName: indexSchema.name,
|
|
805
|
+
ddl: generateIndexDDL(indexSchema, updatedSchema),
|
|
754
806
|
});
|
|
755
807
|
}
|
|
756
808
|
/**
|
|
@@ -808,6 +860,7 @@ export class StoreModule {
|
|
|
808
860
|
objectType: 'index',
|
|
809
861
|
schemaName,
|
|
810
862
|
objectName: indexName,
|
|
863
|
+
ddl: generateDropIndexDDL(schemaName, indexName),
|
|
811
864
|
});
|
|
812
865
|
}
|
|
813
866
|
/**
|
|
@@ -841,6 +894,11 @@ export class StoreModule {
|
|
|
841
894
|
const encodeOptions = { collation: keyCollation, normalizers };
|
|
842
895
|
const pkDirections = tableSchema.primaryKeyDefinition.map(pk => !!pk.desc);
|
|
843
896
|
const pkCollations = resolvePkKeyCollations(tableSchema.primaryKeyDefinition, tableSchema.columns, keyCollation);
|
|
897
|
+
// Key-identity transforms for both halves, mirroring `StoreTable`'s maintenance
|
|
898
|
+
// writes (`updateSecondaryIndexes` / `encodeDataKey`) — a rebuild that skipped them
|
|
899
|
+
// would re-encode a TIMESPAN member under different bytes than DML writes.
|
|
900
|
+
const pkTransforms = resolvePkKeyTransforms(tableSchema.primaryKeyDefinition, tableSchema.columns);
|
|
901
|
+
const indexTransforms = resolveIndexKeyTransforms(indexSchema, tableSchema.columns);
|
|
844
902
|
const indexDirections = indexSchema.columns.map(col => !!col.desc);
|
|
845
903
|
const predicate = indexSchema.predicate
|
|
846
904
|
? compilePredicate(indexSchema.predicate, tableSchema.columns, tableSchema.name)
|
|
@@ -868,9 +926,9 @@ export class StoreModule {
|
|
|
868
926
|
// Extract index column values
|
|
869
927
|
const indexValues = indexSchema.columns.map(col => row[col.index]);
|
|
870
928
|
if (seen) {
|
|
871
|
-
//
|
|
929
|
+
// The signature returns null when any indexed column is NULL —
|
|
872
930
|
// SQL UNIQUE allows multiple NULLs, so those rows never collide.
|
|
873
|
-
const keySig =
|
|
931
|
+
const keySig = dedupeRowSignature(row, indexColIndices, indexNormalizers, indexTransforms);
|
|
874
932
|
if (keySig !== null) {
|
|
875
933
|
if (seen.has(keySig)) {
|
|
876
934
|
const colNames = indexSchema.columns
|
|
@@ -882,13 +940,13 @@ export class StoreModule {
|
|
|
882
940
|
}
|
|
883
941
|
}
|
|
884
942
|
// Build and store index key
|
|
885
|
-
const indexKey = buildIndexKey(indexValues, pkValues, encodeOptions, indexDirections, pkDirections, pkCollations);
|
|
943
|
+
const indexKey = buildIndexKey(indexValues, pkValues, encodeOptions, indexDirections, pkDirections, pkCollations, indexTransforms, pkTransforms);
|
|
886
944
|
// Index value = the row's encoded DATA key, so an index scan resolves each
|
|
887
945
|
// entry back to its base row via a direct data-store read (see
|
|
888
946
|
// `StoreTable.scanIndex`). Encoded under the same PK directions + per-column
|
|
889
|
-
// PK collations as `buildDataKey` / `updateSecondaryIndexes`,
|
|
890
|
-
// byte-matches the data store's key for this row.
|
|
891
|
-
const dataKey = buildDataKey(pkValues, encodeOptions, pkDirections, pkCollations);
|
|
947
|
+
// PK collations + key transforms as `buildDataKey` / `updateSecondaryIndexes`,
|
|
948
|
+
// so the value byte-matches the data store's key for this row.
|
|
949
|
+
const dataKey = buildDataKey(pkValues, encodeOptions, pkDirections, pkCollations, pkTransforms);
|
|
892
950
|
batch.put(indexKey, dataKey);
|
|
893
951
|
// Bound heap: once the accumulated serialized key bytes cross the budget, flush
|
|
894
952
|
// the batch and start a fresh one. Both callers ITERATE the data store and WRITE
|
|
@@ -920,11 +978,20 @@ export class StoreModule {
|
|
|
920
978
|
* connection's `db.getKeyNormalizerResolver()` — see {@link buildIndexEntries}.
|
|
921
979
|
*
|
|
922
980
|
* NOTE: reads the data store committed-only and writes the index stores outside the
|
|
923
|
-
* coordinator. Sound only because
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
|
|
927
|
-
|
|
981
|
+
* coordinator. Sound only because every caller calls {@link ddlCommitPendingOps}
|
|
982
|
+
* first, so this module's "committed" is "everything live". A caller that skips that
|
|
983
|
+
* flush would rebuild an index missing its transaction's pending rows.
|
|
984
|
+
*
|
|
985
|
+
* `skipDuplicateCheck` suppresses {@link buildIndexEntries}' in-pass UNIQUE check.
|
|
986
|
+
* The value-rewriting / key-transform ALTER COLUMN arm sets it: its pre-mutation
|
|
987
|
+
* UNIQUE re-validation already judged the ISSUING connection's effective rows (a
|
|
988
|
+
* wrapper's overlay included), and this rebuild sees only THIS module's committed
|
|
989
|
+
* rows — a superset that may retain a row the wrapper's transaction has deleted, so
|
|
990
|
+
* judging it here would spuriously reject a duplicate pair the probe correctly
|
|
991
|
+
* accepted. Mirrors the memory module's deliberately non-enforcing base rebuild
|
|
992
|
+
* (the probe is the only guard).
|
|
993
|
+
*/
|
|
994
|
+
async rebuildSecondaryIndexes(schemaName, tableName, table, schema, normalizers, skipDuplicateCheck = false) {
|
|
928
995
|
const keyCollation = (table.getConfig().collation || 'NOCASE').toUpperCase();
|
|
929
996
|
const dataStore = await this.getStore(schemaName, tableName, table.getConfig());
|
|
930
997
|
const maxBatchBytes = table.getConfig().maxBatchBytes ?? DEFAULT_MAX_BATCH_BYTES;
|
|
@@ -942,7 +1009,7 @@ export class StoreModule {
|
|
|
942
1009
|
clearBatch.delete(entry.key);
|
|
943
1010
|
}
|
|
944
1011
|
await clearBatch.write();
|
|
945
|
-
await this.buildIndexEntries(dataStore.iterate(buildFullScanBounds()), indexStore, schema, indexSchema, keyCollation, normalizers,
|
|
1012
|
+
await this.buildIndexEntries(dataStore.iterate(buildFullScanBounds()), indexStore, schema, indexSchema, keyCollation, normalizers, skipDuplicateCheck, maxBatchBytes);
|
|
946
1013
|
}
|
|
947
1014
|
}
|
|
948
1015
|
/**
|
|
@@ -965,7 +1032,7 @@ export class StoreModule {
|
|
|
965
1032
|
*
|
|
966
1033
|
* No index store is written — store UNIQUE enforcement is a full-scan over
|
|
967
1034
|
* `uniqueConstraints` at write time. The signature is built by
|
|
968
|
-
* {@link
|
|
1035
|
+
* {@link dedupeRowSignature} with one normalizer per constrained column, resolved from
|
|
969
1036
|
* `tableSchema.columns[idx].collation` through the connection's
|
|
970
1037
|
* `db.getKeyNormalizerResolver()`, so a per-column collation registered with
|
|
971
1038
|
* `db.registerCollation` is honored (matching write-time `compareSqlValues`
|
|
@@ -1183,8 +1250,16 @@ export class StoreModule {
|
|
|
1183
1250
|
* ADD COLUMN arm of {@link alterTable}: append the new column, eagerly migrate
|
|
1184
1251
|
* each row (literal or per-row backfill), and persist. Behavior-preserving
|
|
1185
1252
|
* extraction of the former `switch` arm.
|
|
1253
|
+
*
|
|
1254
|
+
* The store always appends. A caller-chosen `insertAtIndex` (module-API only; SQL never
|
|
1255
|
+
* produces one) is rejected unless it names the append position, rather than silently
|
|
1256
|
+
* landing the column somewhere the caller did not ask for.
|
|
1186
1257
|
*/
|
|
1187
1258
|
async alterAddColumn(db, schemaName, tableName, table, oldSchema, change, defaultNotNull) {
|
|
1259
|
+
if (change.insertAtIndex !== undefined && change.insertAtIndex !== oldSchema.columns.length) {
|
|
1260
|
+
throw new QuereusError(`Store-backed table '${schemaName}.${tableName}' can only ADD COLUMN at the end `
|
|
1261
|
+
+ `(position ${oldSchema.columns.length}), not at position ${change.insertAtIndex}`, StatusCode.UNSUPPORTED);
|
|
1262
|
+
}
|
|
1188
1263
|
// Honor the session `default_collation` for an ADD COLUMN that omits an
|
|
1189
1264
|
// explicit COLLATE, matching the CREATE path so an ADD-COLUMN-ed text column
|
|
1190
1265
|
// gets the same collation a CREATE-d one would. The persisted DDL re-emits an
|
|
@@ -1221,19 +1296,11 @@ export class StoreModule {
|
|
|
1221
1296
|
columns: updatedColumns,
|
|
1222
1297
|
columnIndexMap: buildColumnIndexMap(updatedColumns),
|
|
1223
1298
|
};
|
|
1224
|
-
// Extract any column-level CHECK / FK to persist (see the persist block below).
|
|
1225
|
-
// Hoisted above the row migration so a malformed constraint (e.g. a multi-column
|
|
1226
|
-
// FK on a single ADD COLUMN, which `extractColumnLevelForeignKeys` rejects) throws
|
|
1227
|
-
// BEFORE any rows are migrated or the in-memory schema is swapped — validate-before-
|
|
1228
|
-
// mutate, matching the engine's ordering in `runAddColumn`.
|
|
1229
|
-
const newCheckConstraints = extractColumnLevelCheckConstraints(change.columnDef);
|
|
1230
|
-
const newForeignKeys = extractColumnLevelForeignKeys(change.columnDef, schemaName);
|
|
1231
1299
|
// Physical rewrite ahead: flush the module's buffered writes so `migrateRows`
|
|
1232
1300
|
// (a committed-store scan + batch) sees this transaction's rows and re-encodes
|
|
1233
1301
|
// them under the new column layout. See {@link ddlCommitPendingOps}. Placed
|
|
1234
1302
|
// after every throw-only check above — the NOT NULL rejection reads effectively
|
|
1235
|
-
// (`hasAnyRows`)
|
|
1236
|
-
// leaves the enclosing transaction intact.
|
|
1303
|
+
// (`hasAnyRows`) — so a rejected ALTER leaves the enclosing transaction intact.
|
|
1237
1304
|
await this.ddlCommitPendingOps();
|
|
1238
1305
|
// Migrate rows: append the new column's value — a single literal default, or a
|
|
1239
1306
|
// per-row value derived from the existing row when a backfill evaluator is set.
|
|
@@ -1241,33 +1308,13 @@ export class StoreModule {
|
|
|
1241
1308
|
await table.migrateRows(remap, defaultValue, backfillEvaluator
|
|
1242
1309
|
? { evaluator: backfillEvaluator, notNull: newColSchema.notNull, columnName: newColSchema.name }
|
|
1243
1310
|
: undefined);
|
|
1244
|
-
// Update table schema (column-only) and persist DDL.
|
|
1245
|
-
//
|
|
1246
|
-
//
|
|
1247
|
-
//
|
|
1248
|
-
//
|
|
1249
|
-
// constrained schema would double the constraint in the live SchemaManager (and,
|
|
1250
|
-
// on the next persist, in the DDL). But that engine-side merge is in-memory only:
|
|
1251
|
-
// it never reaches the catalog, so persistence must carry the column-level
|
|
1252
|
-
// CHECK/FK itself or they vanish on `rehydrateCatalog`. Build a separate
|
|
1253
|
-
// `persistedSchema` for `saveTableDDL` when (and only when) the column declares
|
|
1254
|
-
// such a constraint; the common path persists `updatedSchema` unchanged. This is
|
|
1255
|
-
// unconditional on the default kind — a per-row (evaluator) DEFAULT extracts the
|
|
1256
|
-
// same AST constraints as a literal one.
|
|
1311
|
+
// Update table schema (column-only) and persist DDL. Any constraint declared inline
|
|
1312
|
+
// on the added column (UNIQUE / CHECK / FOREIGN KEY) arrives as its own follow-up
|
|
1313
|
+
// `addConstraint` call from the engine's `runAddColumn`, which is what persists it —
|
|
1314
|
+
// so this arm neither installs nor persists them, and the schema it hands back stays
|
|
1315
|
+
// column-only.
|
|
1257
1316
|
table.updateSchema(updatedSchema);
|
|
1258
|
-
|
|
1259
|
-
if (newCheckConstraints.length > 0 || newForeignKeys.length > 0) {
|
|
1260
|
-
// The new column is appended last; resolve each FK's child column to its index
|
|
1261
|
-
// (matching how the engine resolves `resolvedForeignKeys` via columnIndexMap).
|
|
1262
|
-
const newColIdx = updatedColumns.length - 1;
|
|
1263
|
-
const resolvedForeignKeys = newForeignKeys.map(fk => ({ ...fk, columns: Object.freeze([newColIdx]) }));
|
|
1264
|
-
persistedSchema = {
|
|
1265
|
-
...updatedSchema,
|
|
1266
|
-
checkConstraints: Object.freeze([...updatedSchema.checkConstraints, ...newCheckConstraints]),
|
|
1267
|
-
foreignKeys: Object.freeze([...(updatedSchema.foreignKeys ?? []), ...resolvedForeignKeys]),
|
|
1268
|
-
};
|
|
1269
|
-
}
|
|
1270
|
-
await this.saveTableDDL(persistedSchema);
|
|
1317
|
+
await this.saveTableDDL(updatedSchema);
|
|
1271
1318
|
this.eventEmitter?.emitSchemaChange({
|
|
1272
1319
|
type: 'alter',
|
|
1273
1320
|
objectType: 'table',
|
|
@@ -1313,6 +1360,18 @@ export class StoreModule {
|
|
|
1313
1360
|
const updatedUniqueConstraints = (oldSchema.uniqueConstraints ?? [])
|
|
1314
1361
|
.filter(uc => !uc.columns.includes(colIndex))
|
|
1315
1362
|
.map(uc => ({ ...uc, columns: Object.freeze(uc.columns.map(i => i > colIndex ? i - 1 : i)) }));
|
|
1363
|
+
// Shift or prune this table's own foreign keys, mirroring the UNIQUE pruning above
|
|
1364
|
+
// and the memory module's `dropColumn`. `foreignKeys[].columns` are indices into
|
|
1365
|
+
// THIS table, so they renumber over the removed slot; a key that loses ANY of its
|
|
1366
|
+
// child columns is removed outright (a key missing a child column is a different
|
|
1367
|
+
// constraint against the parent's key, not a narrowed one). Left unshifted, a
|
|
1368
|
+
// surviving key's index dangles past the column array or silently slides onto an
|
|
1369
|
+
// unrelated column — and `saveTableDDL` below persists whichever it ends up being.
|
|
1370
|
+
// `referencedColumnNames` is resolved against the parent by name at enforcement
|
|
1371
|
+
// time, so the parent side needs nothing here.
|
|
1372
|
+
const updatedForeignKeys = (oldSchema.foreignKeys ?? [])
|
|
1373
|
+
.filter(fk => !fk.columns.includes(colIndex))
|
|
1374
|
+
.map(fk => ({ ...fk, columns: Object.freeze(fk.columns.map(i => i > colIndex ? i - 1 : i)) }));
|
|
1316
1375
|
const updatedSchema = {
|
|
1317
1376
|
...oldSchema,
|
|
1318
1377
|
columns: Object.freeze(updatedColumns),
|
|
@@ -1322,6 +1381,9 @@ export class StoreModule {
|
|
|
1322
1381
|
uniqueConstraints: updatedUniqueConstraints.length > 0
|
|
1323
1382
|
? Object.freeze(updatedUniqueConstraints)
|
|
1324
1383
|
: undefined,
|
|
1384
|
+
foreignKeys: updatedForeignKeys.length > 0
|
|
1385
|
+
? Object.freeze(updatedForeignKeys)
|
|
1386
|
+
: undefined,
|
|
1325
1387
|
};
|
|
1326
1388
|
// Physical rewrite ahead — flush buffered writes so `migrateRows` re-encodes
|
|
1327
1389
|
// this transaction's rows too. See {@link ddlCommitPendingOps}.
|
|
@@ -1581,16 +1643,17 @@ export class StoreModule {
|
|
|
1581
1643
|
const oldCol = oldSchema.columns[colIndex];
|
|
1582
1644
|
// Pull exactly one attribute from the change. Each sub-helper returns the new
|
|
1583
1645
|
// column schema plus whether the collation bytes changed (`collationChanged` gates
|
|
1584
|
-
// the existing-row UNIQUE re-validation and PK re-key below)
|
|
1585
|
-
//
|
|
1586
|
-
//
|
|
1587
|
-
//
|
|
1646
|
+
// the existing-row UNIQUE re-validation and PK re-key below) plus a DEFERRED
|
|
1647
|
+
// `valueConvert` closure when the change rewrites stored values — the sub-helper
|
|
1648
|
+
// itself mutates nothing. Or null when the column is already in the desired state —
|
|
1649
|
+
// preserving the pre-refactor early exits (SET NOT NULL no-op, SET COLLATE
|
|
1650
|
+
// already-explicit) that leave the table and any open transaction untouched.
|
|
1588
1651
|
let attr;
|
|
1589
1652
|
if (change.setNotNull !== undefined) {
|
|
1590
1653
|
attr = await this.alterColumnSetNotNull(table, oldSchema, oldCol, colIndex, change, rows);
|
|
1591
1654
|
}
|
|
1592
1655
|
else if (change.setDataType !== undefined) {
|
|
1593
|
-
attr = await this.alterColumnSetDataType(table, oldCol, colIndex, change);
|
|
1656
|
+
attr = await this.alterColumnSetDataType(table, oldSchema, oldCol, colIndex, change);
|
|
1594
1657
|
}
|
|
1595
1658
|
else if (change.setDefault !== undefined) {
|
|
1596
1659
|
attr = { newCol: { ...oldCol, defaultValue: change.setDefault }, collationChanged: false };
|
|
@@ -1604,7 +1667,12 @@ export class StoreModule {
|
|
|
1604
1667
|
if (attr === null) {
|
|
1605
1668
|
return oldSchema;
|
|
1606
1669
|
}
|
|
1607
|
-
const { newCol, collationChanged,
|
|
1670
|
+
const { newCol, collationChanged, valueConvert } = attr;
|
|
1671
|
+
// Named for the PENDING act, not a completed one: the deferred value rewrite (SET DATA
|
|
1672
|
+
// TYPE conversion / SET NOT NULL backfill) is applied below, after every throw-only
|
|
1673
|
+
// check. Both gates that read this run BEFORE it, while the store still holds the old
|
|
1674
|
+
// values.
|
|
1675
|
+
const rewritesValues = valueConvert !== undefined;
|
|
1608
1676
|
const updatedColumns = oldSchema.columns.map((c, i) => i === colIndex ? newCol : c);
|
|
1609
1677
|
// Mirror the memory module (MemoryTableManager.alterColumn): a per-column
|
|
1610
1678
|
// collation change propagates into every index column ordering by this
|
|
@@ -1630,21 +1698,47 @@ export class StoreModule {
|
|
|
1630
1698
|
columnIndexMap: buildColumnIndexMap(updatedColumns),
|
|
1631
1699
|
indexes: updatedIndexes ? Object.freeze(updatedIndexes) : updatedIndexes,
|
|
1632
1700
|
};
|
|
1633
|
-
// SET
|
|
1634
|
-
//
|
|
1635
|
-
//
|
|
1636
|
-
//
|
|
1637
|
-
//
|
|
1638
|
-
//
|
|
1701
|
+
// SET DATA TYPE onto/off a type with a key transform (TIMESPAN's total-seconds
|
|
1702
|
+
// groupKey, JSON's structural encoder — see `storeSemanticKeyTransform`)
|
|
1703
|
+
// changes the physical KEY BYTES a value encodes to, even when the stored
|
|
1704
|
+
// VALUE bytes don't change (text → timespan keeps TEXT physical). Treated
|
|
1705
|
+
// exactly like a collation change below: existing-row UNIQUE re-validation
|
|
1706
|
+
// (equal-elapsed spellings now collide), a PK re-key when the column is a PK
|
|
1707
|
+
// member, and a secondary-index rebuild (index keys encode the column's
|
|
1708
|
+
// values). A same-type no-op ALTER never gets here (`inferType` returns the
|
|
1709
|
+
// shared type object, so the identity check short-circuits).
|
|
1710
|
+
const keyTransformChanged = oldCol.logicalType !== newCol.logicalType
|
|
1711
|
+
&& (storeSemanticKeyTransform(oldCol.logicalType) !== undefined
|
|
1712
|
+
|| storeSemanticKeyTransform(newCol.logicalType) !== undefined);
|
|
1713
|
+
// Existing-row UNIQUE re-validation (Option A, non-PK UNIQUE). Two ways an ALTER
|
|
1714
|
+
// COLUMN can make rows that were distinct collide, both judged here BEFORE any
|
|
1715
|
+
// mutation/persist so the first collision throws CONSTRAINT and leaves the table
|
|
1716
|
+
// unchanged and writable (matches the ADD CONSTRAINT rollback shape):
|
|
1717
|
+
// - SET COLLATE / a key-transform change: the comparators change ('a'/'A' collide
|
|
1718
|
+
// under NOCASE). Re-scan under the NEW collation (`updatedSchema` carries it).
|
|
1719
|
+
// - a value-REWRITING change (`valueConvert`: SET DATA TYPE, or a SET NOT NULL
|
|
1720
|
+
// null → DEFAULT backfill): the comparators are unchanged but the VALUES collapse
|
|
1721
|
+
// ('1'/'01' → 1; two NULLs → the same DEFAULT). The probe judges the CONVERTED
|
|
1722
|
+
// rows — the same closure the deferred rewrite below applies — while the store
|
|
1723
|
+
// still holds the old values. A standalone `create unique index` synthesizes a
|
|
1724
|
+
// `derivedFromIndex` entry in `uniqueConstraints`, so both declaration shapes are
|
|
1725
|
+
// reachable through this walk.
|
|
1639
1726
|
// The PK is intentionally excluded — it never appears in `uniqueConstraints`;
|
|
1640
|
-
// its physical re-key/re-validation is the `
|
|
1641
|
-
|
|
1727
|
+
// its physical re-key/re-validation is the `pkRekeyNeeded` block below.
|
|
1728
|
+
//
|
|
1729
|
+
// NOTE: one full row scan per covering constraint, and this gate now fires on the
|
|
1730
|
+
// COMMON retype/backfill path, not only on the rare collation/transform ones. Fine
|
|
1731
|
+
// while tables covered by several UNIQUE constraints over one column are rare; if a
|
|
1732
|
+
// wide-constraint table makes ALTER COLUMN slow, judge all covering constraints in a
|
|
1733
|
+
// single pass (one `seen` Set per constraint, one stream).
|
|
1734
|
+
if (collationChanged || keyTransformChanged || rewritesValues) {
|
|
1642
1735
|
const coveringConstraints = (updatedSchema.uniqueConstraints ?? [])
|
|
1643
1736
|
.filter(uc => uc.columns.includes(colIndex));
|
|
1644
1737
|
for (const uc of coveringConstraints) {
|
|
1645
1738
|
// Fresh generator per constraint — an async generator is single-shot. An
|
|
1646
1739
|
// `EffectiveRowSource` is re-callable for exactly this reason.
|
|
1647
|
-
|
|
1740
|
+
const effectiveRows = rows ? rows() : rowsFromEntries(table.iterateEffectiveEntries(buildFullScanBounds()));
|
|
1741
|
+
await this.validateUniqueOverExistingRows(valueConvert ? convertRowsAtIndex(effectiveRows, colIndex, valueConvert) : effectiveRows, updatedSchema, uc, db.getKeyNormalizerResolver());
|
|
1648
1742
|
}
|
|
1649
1743
|
}
|
|
1650
1744
|
// SET COLLATE on a PRIMARY KEY member (Option B physical re-key): re-encode
|
|
@@ -1656,7 +1750,9 @@ export class StoreModule {
|
|
|
1656
1750
|
// ALTER PRIMARY KEY. Runs AFTER the non-PK UNIQUE re-validation above so both
|
|
1657
1751
|
// throw-only checks precede the first store mutation. `updatedSchema.columns`
|
|
1658
1752
|
// carries the new collation, so the new key bytes follow it.
|
|
1659
|
-
|
|
1753
|
+
const pkRekeyNeeded = (collationChanged || keyTransformChanged)
|
|
1754
|
+
&& oldSchema.primaryKeyDefinition.some(def => def.index === colIndex);
|
|
1755
|
+
if (pkRekeyNeeded) {
|
|
1660
1756
|
// Physical re-key ahead — flush buffered writes (see
|
|
1661
1757
|
// {@link ddlCommitPendingOps}). Deliberately AFTER the non-PK UNIQUE
|
|
1662
1758
|
// re-validation above: that check reads effectively and throws without
|
|
@@ -1674,18 +1770,46 @@ export class StoreModule {
|
|
|
1674
1770
|
// key collation too (`updatedSchema` carries none on its own).
|
|
1675
1771
|
await this.rebuildSecondaryIndexes(schemaName, tableName, table, withImplicitUniqueIndexes(updatedSchema), db.getKeyNormalizerResolver());
|
|
1676
1772
|
}
|
|
1677
|
-
// SET DATA TYPE physical conversion / SET NOT NULL DEFAULT
|
|
1678
|
-
//
|
|
1679
|
-
//
|
|
1680
|
-
//
|
|
1681
|
-
//
|
|
1682
|
-
//
|
|
1683
|
-
//
|
|
1684
|
-
//
|
|
1685
|
-
//
|
|
1686
|
-
//
|
|
1687
|
-
|
|
1688
|
-
|
|
1773
|
+
// Deferred value rewrite (SET DATA TYPE physical conversion / SET NOT NULL DEFAULT
|
|
1774
|
+
// backfill): every throw-only check above has passed, so the store mutation is now
|
|
1775
|
+
// safe — a rejected ALTER never reaches this point, leaving values, DDL and the
|
|
1776
|
+
// transaction untouched. Flush buffered writes first so `mapRowsAtIndex` rewrites this
|
|
1777
|
+
// transaction's rows too; unflushed, they would replay under the OLD physical type
|
|
1778
|
+
// (see {@link ddlCommitPendingOps}; a re-flush after the PK re-key's own is a no-op).
|
|
1779
|
+
//
|
|
1780
|
+
// NOTE: `valueConvert` and `pkRekeyNeeded` cannot both be set today, and this ordering
|
|
1781
|
+
// depends on that. `valueConvert` comes only from SET DATA TYPE or SET NOT NULL;
|
|
1782
|
+
// SET NOT NULL changes neither collation nor logical type, so it never sets
|
|
1783
|
+
// `pkRekeyNeeded`, and SET DATA TYPE on a PK member is refused both upstream (every live
|
|
1784
|
+
// caller — the engine's ALTER COLUMN emitter, see `runAlterColumn` in
|
|
1785
|
+
// runtime/emit/alter-table.ts, and the materialized-view reshape, which declares a
|
|
1786
|
+
// key-column retype inexpressible) AND locally, by `alterColumnSetDataType` itself. If a
|
|
1787
|
+
// PK-member retype is ever admitted (that local guard removed), this rewrite must move IN
|
|
1788
|
+
// FRONT of the `pkRekeyNeeded` block: otherwise `rekeyRows` and its index rebuild encode
|
|
1789
|
+
// the PRE-rewrite values and the rebuild below is skipped, leaving keys and indexes
|
|
1790
|
+
// disagreeing with the stored values.
|
|
1791
|
+
if (valueConvert) {
|
|
1792
|
+
await this.ddlCommitPendingOps();
|
|
1793
|
+
await table.mapRowsAtIndex(colIndex, valueConvert);
|
|
1794
|
+
}
|
|
1795
|
+
// The value rewrite above changed stored column values in place (same PK, new value) —
|
|
1796
|
+
// and a key-identity transform change re-encodes the column's index-key bytes even with
|
|
1797
|
+
// values untouched. Secondary index KEY bytes encode the indexed column VALUES, so any
|
|
1798
|
+
// index covering this column still points at the OLD bytes until rebuilt — an
|
|
1799
|
+
// index-backed lookup for the new value finds nothing (mirrors the memory module's
|
|
1800
|
+
// `valueConvert` rebuild in MemoryTableManager.alterColumn). `rebuildSecondaryIndexes`
|
|
1801
|
+
// reads committed-only, so flush buffered writes first — the value rewrite above
|
|
1802
|
+
// already did, and the transform-only path (text → timespan: no value rewrite) must
|
|
1803
|
+
// too; a re-flush with nothing pending is a no-op. Skipped when the PK re-key above
|
|
1804
|
+
// already rebuilt every index, so no double rebuild. Materialize so an implicit `_uc_*`
|
|
1805
|
+
// over the rewritten column is rebuilt against the new value bytes too. The rebuild is
|
|
1806
|
+
// NON-enforcing (`skipDuplicateCheck`): the UNIQUE re-validation above already judged
|
|
1807
|
+
// the issuer's effective rows, and this module's committed rows may retain a row a
|
|
1808
|
+
// wrapper's transaction has deleted whose converted value duplicates a survivor —
|
|
1809
|
+
// enforcing here would spuriously reject what the probe correctly accepted.
|
|
1810
|
+
if ((rewritesValues || keyTransformChanged) && !pkRekeyNeeded) {
|
|
1811
|
+
await this.ddlCommitPendingOps();
|
|
1812
|
+
await this.rebuildSecondaryIndexes(schemaName, tableName, table, withImplicitUniqueIndexes(updatedSchema), db.getKeyNormalizerResolver(), true);
|
|
1689
1813
|
}
|
|
1690
1814
|
table.updateSchema(updatedSchema);
|
|
1691
1815
|
await this.saveTableDDL(updatedSchema);
|
|
@@ -1700,8 +1824,10 @@ export class StoreModule {
|
|
|
1700
1824
|
/**
|
|
1701
1825
|
* SET NOT NULL / DROP NOT NULL sub-branch of {@link alterColumnChange}. Returns the
|
|
1702
1826
|
* new column schema, or null when the column is already in the desired nullability
|
|
1703
|
-
* (the pre-refactor `return oldSchema` no-op).
|
|
1704
|
-
*
|
|
1827
|
+
* (the pre-refactor `return oldSchema` no-op). Mutates nothing: the NULL-backfill
|
|
1828
|
+
* probe is throw-only, and the backfill itself is returned as a deferred
|
|
1829
|
+
* `valueConvert` (null → DEFAULT) the caller applies only after every throw-only
|
|
1830
|
+
* check — including the UNIQUE re-validation over the backfilled values — has passed.
|
|
1705
1831
|
*
|
|
1706
1832
|
* `rows` is the wrapper-supplied effective row source (the isolation overlay). When present,
|
|
1707
1833
|
* the reject-vs-backfill decision scans it instead of `table.rowsWithNullAtIndex`: behind the
|
|
@@ -1712,7 +1838,7 @@ export class StoreModule {
|
|
|
1712
1838
|
*/
|
|
1713
1839
|
async alterColumnSetNotNull(table, oldSchema, oldCol, colIndex, change, rows) {
|
|
1714
1840
|
let newCol;
|
|
1715
|
-
let
|
|
1841
|
+
let valueConvert;
|
|
1716
1842
|
if (change.setNotNull === true && !oldCol.notNull) {
|
|
1717
1843
|
// Backfill NULLs from a literal DEFAULT, or throw.
|
|
1718
1844
|
let defaultLiteral;
|
|
@@ -1743,14 +1869,12 @@ export class StoreModule {
|
|
|
1743
1869
|
throw new QuereusError(`column ${change.columnName} contains NULL values`, StatusCode.CONSTRAINT);
|
|
1744
1870
|
}
|
|
1745
1871
|
const fill = defaultLiteral;
|
|
1746
|
-
//
|
|
1747
|
-
//
|
|
1748
|
-
// probe saw them). Behind isolation the issuer's overlay-resident
|
|
1749
|
-
// filled by the isolation layer's overlay migration, not here —
|
|
1750
|
-
//
|
|
1751
|
-
|
|
1752
|
-
await table.mapRowsAtIndex(colIndex, (v) => v === null ? fill : v);
|
|
1753
|
-
valuesRewritten = true;
|
|
1872
|
+
// Backfill DEFERRED: the caller flushes + `mapRowsAtIndex`es this closure only
|
|
1873
|
+
// after its throw-only checks pass. Run directly, that fills the transaction's
|
|
1874
|
+
// rows too (the probe saw them). Behind isolation the issuer's overlay-resident
|
|
1875
|
+
// NULL rows are filled by the isolation layer's overlay migration, not here —
|
|
1876
|
+
// this store never holds them.
|
|
1877
|
+
valueConvert = (v) => v === null ? fill : v;
|
|
1754
1878
|
}
|
|
1755
1879
|
newCol = { ...oldCol, notNull: true };
|
|
1756
1880
|
}
|
|
@@ -1763,18 +1887,40 @@ export class StoreModule {
|
|
|
1763
1887
|
else {
|
|
1764
1888
|
return null; // already in desired state
|
|
1765
1889
|
}
|
|
1766
|
-
return { newCol, collationChanged: false,
|
|
1890
|
+
return { newCol, collationChanged: false, valueConvert };
|
|
1767
1891
|
}
|
|
1768
1892
|
/**
|
|
1769
1893
|
* SET DATA TYPE sub-branch of {@link alterColumnChange}. Returns the retyped column
|
|
1770
|
-
* schema.
|
|
1771
|
-
*
|
|
1772
|
-
|
|
1773
|
-
|
|
1894
|
+
* schema. Mutates nothing: for every retype between DIFFERENT logical types, a
|
|
1895
|
+
* throw-only convert pass over the live rows proves every value convertible, and the
|
|
1896
|
+
* conversion itself is returned as a deferred `valueConvert` the caller applies only
|
|
1897
|
+
* after every throw-only check — including the UNIQUE re-validation over the converted
|
|
1898
|
+
* values — has passed. Gated on logical-type IDENTITY, not the physical storage class:
|
|
1899
|
+
* `inferType` flattens aliases to the shared type object (`varchar(50)` IS `TEXT_TYPE`),
|
|
1900
|
+
* so an alias retype is schema-only, while a same-class retype (text → date) still
|
|
1901
|
+
* rejects values the new type refuses and rewrites the rest to the new type's
|
|
1902
|
+
* canonical spelling ('2024-06-05T00:00:00Z' → '2024-06-05') — exactly as an INSERT
|
|
1903
|
+
* would have stored them.
|
|
1904
|
+
*
|
|
1905
|
+
* A retype of a PRIMARY KEY member is refused here rather than converted: the rewrite
|
|
1906
|
+
* below is `mapRowsAtIndex`, a payload-only rewrite that reuses `entry.key` verbatim, so
|
|
1907
|
+
* a PK column's physical key bytes would stay encoded under the OLD type while the value
|
|
1908
|
+
* moves to the new one — unfindable by any lookup under the new encoding, and the same
|
|
1909
|
+
* `keyTransformChanged` path in the caller would re-key from the pre-rewrite values before
|
|
1910
|
+
* this rewrite ever ran (see the NOTE above the `valueConvert` block in
|
|
1911
|
+
* {@link alterColumnChange}). Every live SQL caller already refuses this earlier (the
|
|
1912
|
+
* engine's `runAlterColumn` and the materialized-view reshape's inexpressibility check),
|
|
1913
|
+
* so this only guards a direct module call — mirrors the memory backend's carve-out
|
|
1914
|
+
* (`MemoryTableManager.alterColumn`).
|
|
1915
|
+
*/
|
|
1916
|
+
async alterColumnSetDataType(table, oldSchema, oldCol, colIndex, change) {
|
|
1774
1917
|
const newLogicalType = inferType(change.setDataType);
|
|
1775
|
-
let
|
|
1776
|
-
if (newLogicalType
|
|
1777
|
-
|
|
1918
|
+
let valueConvert;
|
|
1919
|
+
if (newLogicalType !== oldCol.logicalType) {
|
|
1920
|
+
if (oldSchema.primaryKeyDefinition.some(def => def.index === colIndex)) {
|
|
1921
|
+
throw new QuereusError(`Cannot change the data type of primary key column '${change.columnName}' of table '${oldSchema.name}'.`, StatusCode.CONSTRAINT);
|
|
1922
|
+
}
|
|
1923
|
+
// Conversion required — walk every row and attempt parse.
|
|
1778
1924
|
const convert = (v) => {
|
|
1779
1925
|
try {
|
|
1780
1926
|
return validateAndParse(v, newLogicalType, change.columnName);
|
|
@@ -1783,21 +1929,21 @@ export class StoreModule {
|
|
|
1783
1929
|
throw new QuereusError(`Cannot convert value in '${change.columnName}' to ${change.setDataType}`, StatusCode.MISMATCH);
|
|
1784
1930
|
}
|
|
1785
1931
|
};
|
|
1786
|
-
// Throw-only pass over the LIVE rows
|
|
1787
|
-
//
|
|
1788
|
-
//
|
|
1932
|
+
// Throw-only pass over the LIVE rows, so an unconvertible value this
|
|
1933
|
+
// transaction inserted rejects the ALTER with the transaction still intact.
|
|
1934
|
+
// The rewrite itself is DEFERRED: the caller flushes + `mapRowsAtIndex`es
|
|
1935
|
+
// this closure only after its remaining throw-only checks pass.
|
|
1936
|
+
// NOTE: pre-existing gap — this scan reads the store's own effective rows and
|
|
1937
|
+
// ignores a wrapper-supplied `rows` stream (unlike the memory module's arm). No
|
|
1938
|
+
// hole under isolation: the wrapper's `validateOverlayMigration` converts every
|
|
1939
|
+
// staged overlay value itself before the underlying mutates.
|
|
1789
1940
|
for await (const value of table.iterateEffectiveValuesAtIndex(colIndex)) {
|
|
1790
1941
|
if (value !== null)
|
|
1791
1942
|
convert(value);
|
|
1792
1943
|
}
|
|
1793
|
-
|
|
1794
|
-
// converts this transaction's rows too; unflushed, they would replay
|
|
1795
|
-
// under the OLD physical type. See {@link ddlCommitPendingOps}.
|
|
1796
|
-
await this.ddlCommitPendingOps();
|
|
1797
|
-
await table.mapRowsAtIndex(colIndex, (v) => v === null ? v : convert(v));
|
|
1798
|
-
valuesRewritten = true;
|
|
1944
|
+
valueConvert = (v) => v === null ? v : convert(v);
|
|
1799
1945
|
}
|
|
1800
|
-
return { newCol: { ...oldCol, logicalType: newLogicalType }, collationChanged: false,
|
|
1946
|
+
return { newCol: { ...oldCol, logicalType: newLogicalType }, collationChanged: false, valueConvert };
|
|
1801
1947
|
}
|
|
1802
1948
|
/**
|
|
1803
1949
|
* SET COLLATE sub-branch of {@link alterColumnChange}. Returns the recollated column
|
|
@@ -1928,6 +2074,17 @@ export class StoreModule {
|
|
|
1928
2074
|
}
|
|
1929
2075
|
}
|
|
1930
2076
|
// Move physical storage (data directory + index directories).
|
|
2077
|
+
//
|
|
2078
|
+
// NOTE: the relocation runs BEFORE the catalog rewrite below, and nothing undoes it.
|
|
2079
|
+
// Any failure after this point — an IO error, or the DDL-text guard in
|
|
2080
|
+
// `encodeCatalogDDL` firing on an unpaired surrogate in a column name or a `default`
|
|
2081
|
+
// literal — strands the rows under the NEW physical name while the catalog still
|
|
2082
|
+
// names the old table, so the table reads as empty with only the raised error as a
|
|
2083
|
+
// clue. Harmless for today's validation cases: the store-name guard in
|
|
2084
|
+
// `buildDataStoreName` (called above, before any side effect) refuses a bad target
|
|
2085
|
+
// name outright, and a table whose DDL text is unpersistable can never have held
|
|
2086
|
+
// rows in the first place. If a new post-relocation failure mode appears, the
|
|
2087
|
+
// relocation must be undone here or deferred until after the catalog write.
|
|
1931
2088
|
if (this.provider.renameTableStores) {
|
|
1932
2089
|
await this.provider.renameTableStores(schemaName, oldName, newName, indexNames);
|
|
1933
2090
|
}
|
|
@@ -2463,15 +2620,11 @@ export class StoreModule {
|
|
|
2463
2620
|
}
|
|
2464
2621
|
/**
|
|
2465
2622
|
* Encode a bundle of persisted schema text (DDL) for the catalog store.
|
|
2466
|
-
*
|
|
2467
|
-
*
|
|
2468
|
-
* quoted column name, a `default '…'` string literal, a `check` constraint's string
|
|
2469
|
-
* constant — would otherwise fold to U+FFFD under `TextEncoder` and read back as
|
|
2470
|
-
* different schema text than what was created. This does not rely on the catalog-key
|
|
2471
|
-
* builders' identifier guard having already caught it; it catches it independently.
|
|
2623
|
+
* The guard is {@link assertPersistableDdlText} — see it for why the FULL text
|
|
2624
|
+
* is checked, not just the object's name.
|
|
2472
2625
|
*/
|
|
2473
2626
|
encodeCatalogDDL(ddl) {
|
|
2474
|
-
|
|
2627
|
+
assertPersistableDdlText(ddl);
|
|
2475
2628
|
return new TextEncoder().encode(ddl);
|
|
2476
2629
|
}
|
|
2477
2630
|
/**
|
|
@@ -2792,7 +2945,8 @@ export class StoreModule {
|
|
|
2792
2945
|
* {@link persistObjectCatalogEntryIfChanged}.
|
|
2793
2946
|
*/
|
|
2794
2947
|
async saveViewDDL(view) {
|
|
2795
|
-
|
|
2948
|
+
const { key, ddl } = viewCatalogEntry(view);
|
|
2949
|
+
await this.persistObjectCatalogEntryIfChanged(key, ddl);
|
|
2796
2950
|
}
|
|
2797
2951
|
/** Remove a plain view's catalog entry (on DROP VIEW). */
|
|
2798
2952
|
async removeViewDDL(schemaName, viewName) {
|
|
@@ -2806,13 +2960,50 @@ export class StoreModule {
|
|
|
2806
2960
|
* {@link persistObjectCatalogEntryIfChanged}.
|
|
2807
2961
|
*/
|
|
2808
2962
|
async saveMaterializedViewDDL(mv) {
|
|
2809
|
-
|
|
2963
|
+
const entry = maintainedViewCatalogEntry(mv);
|
|
2964
|
+
// Callers only reach here with a maintained table; the guard keeps the shared
|
|
2965
|
+
// helper's `undefined` arm honest rather than asserting non-null.
|
|
2966
|
+
if (!entry)
|
|
2967
|
+
return;
|
|
2968
|
+
await this.persistObjectCatalogEntryIfChanged(entry.key, entry.ddl);
|
|
2810
2969
|
}
|
|
2811
2970
|
/** Remove a materialized view's catalog entry (on DROP MATERIALIZED VIEW). */
|
|
2812
2971
|
async removeMaterializedViewDDL(schemaName, mvName) {
|
|
2813
2972
|
const catalogStore = await this.provider.getCatalogStore();
|
|
2814
2973
|
await catalogStore.delete(buildMaterializedViewCatalogKey(schemaName, mvName));
|
|
2815
2974
|
}
|
|
2975
|
+
/**
|
|
2976
|
+
* Pre-flight veto (see `VirtualTableModule.assertCatalogObjectPersistable`): refuse a
|
|
2977
|
+
* view / materialized view whose catalog entry this module could not durably write.
|
|
2978
|
+
*
|
|
2979
|
+
* It runs exactly the derivation the write path runs — {@link viewCatalogEntry} /
|
|
2980
|
+
* {@link maintainedViewCatalogEntry} build the key (rejecting an unencodable
|
|
2981
|
+
* identifier) and {@link assertPersistableDdlText} checks the generated DDL text —
|
|
2982
|
+
* so the veto and the write cannot disagree about what is persistable. It is the only
|
|
2983
|
+
* synchronous path a rejection can travel: the actual save is chained onto
|
|
2984
|
+
* `persistQueue` behind a `SchemaChangeNotifier` listener, and both layers swallow.
|
|
2985
|
+
*
|
|
2986
|
+
* Gated on `subscribedDb === db`: this module persists a view/MV only while subscribed
|
|
2987
|
+
* to that `Database`'s change notifier ({@link ensureSchemaSubscription}, driven by the
|
|
2988
|
+
* first `create`/`connect`/`alterTable`/`rehydrateCatalog`). A module registered but
|
|
2989
|
+
* never handed a `db` writes nothing, so vetoing there would reject a definition that
|
|
2990
|
+
* loses nothing. The gate becomes removable if persistence is ever made unconditional
|
|
2991
|
+
* (`bug-store-untouched-table-and-early-view-never-persisted`).
|
|
2992
|
+
*/
|
|
2993
|
+
// NOTE: regenerates the object's full DDL text on every CREATE VIEW / CREATE MATERIALIZED
|
|
2994
|
+
// VIEW / view-or-MV SET TAGS, and the persist that follows regenerates it again. DDL is
|
|
2995
|
+
// rare and the text is small, so the duplicate render is not worth caching today; if a
|
|
2996
|
+
// schema-heavy workload (a large `apply schema` deploying many views) ever shows up hot
|
|
2997
|
+
// here, thread the already-built CatalogEntry from the veto through to the write.
|
|
2998
|
+
assertCatalogObjectPersistable(db, kind, object) {
|
|
2999
|
+
if (this.subscribedDb !== db)
|
|
3000
|
+
return;
|
|
3001
|
+
const entry = kind === 'view'
|
|
3002
|
+
? viewCatalogEntry(object)
|
|
3003
|
+
: maintainedViewCatalogEntry(object);
|
|
3004
|
+
if (entry)
|
|
3005
|
+
assertPersistableDdlText(entry.ddl);
|
|
3006
|
+
}
|
|
2816
3007
|
/**
|
|
2817
3008
|
* Compare-write a view/MV catalog entry: write only when the entry is absent or its
|
|
2818
3009
|
* DDL differs from `newDDL` (skip identical). Unlike the table path's
|
|
@@ -3203,6 +3394,21 @@ async function* rowsFromEntries(entries) {
|
|
|
3203
3394
|
yield deserializeRow(entry.value);
|
|
3204
3395
|
}
|
|
3205
3396
|
}
|
|
3397
|
+
/**
|
|
3398
|
+
* Read-side wrap for a value-rewriting ALTER COLUMN's pre-mutation UNIQUE probe: each row's
|
|
3399
|
+
* value at `colIndex` run through `convert` (the sub-branch's deferred `valueConvert`, which
|
|
3400
|
+
* owns its own NULL handling), so the probe judges the POST-alter values while the store
|
|
3401
|
+
* still holds the old ones. Mutates nothing — the memory module's `convertRowAtIndex`
|
|
3402
|
+
* counterpart. A conversion failure propagates: the setDataType pre-pass already proved
|
|
3403
|
+
* every visible value convertible, so a throw here is unreachable, and surfacing it beats
|
|
3404
|
+
* probing a stale value that could mask a collision.
|
|
3405
|
+
*/
|
|
3406
|
+
async function* convertRowsAtIndex(rows, colIndex, convert) {
|
|
3407
|
+
for await (const row of rows) {
|
|
3408
|
+
const newVal = convert(row[colIndex]);
|
|
3409
|
+
yield newVal === row[colIndex] ? row : row.map((v, i) => i === colIndex ? newVal : v);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3206
3412
|
/**
|
|
3207
3413
|
* Per-column key normalizers for a UNIQUE INDEX dedupe signature, drawing each column's
|
|
3208
3414
|
* collation from the index column (if it carries one) else the underlying table column —
|
|
@@ -3210,7 +3416,7 @@ async function* rowsFromEntries(entries) {
|
|
|
3210
3416
|
* write-time enforcement.
|
|
3211
3417
|
*
|
|
3212
3418
|
* A column whose declared type can never hold text takes the identity normalizer regardless
|
|
3213
|
-
* of its collation (
|
|
3419
|
+
* of its collation (the signature serializer normalizes only string values), so a comparator-only
|
|
3214
3420
|
* collation named on an integer column is not rejected here when the engine's own hash sites
|
|
3215
3421
|
* would accept it.
|
|
3216
3422
|
*/
|
|
@@ -3222,20 +3428,38 @@ function indexDedupeNormalizers(tableSchema, indexSchema, keyNormalizers) {
|
|
|
3222
3428
|
: undefined);
|
|
3223
3429
|
});
|
|
3224
3430
|
}
|
|
3431
|
+
/**
|
|
3432
|
+
* Collation- and identity-aware dedupe signature of `row` over `colIndices`: each value
|
|
3433
|
+
* runs through its column's key transform (`storeSemanticKeyTransform` — TIMESPAN's
|
|
3434
|
+
* total-seconds `groupKey`, so 'PT1H' and 'PT60M' sign identically; JSON's structural
|
|
3435
|
+
* bytes, so reorder-equal objects sign identically) before `serializeKey` applies the
|
|
3436
|
+
* per-column normalizer. Returns null when any covered value
|
|
3437
|
+
* is NULL (SQL UNIQUE allows multiple NULLs). The build/validate-time twin of the
|
|
3438
|
+
* write-time typed compare in `StoreTable.uniqueColumnComparators`.
|
|
3439
|
+
*/
|
|
3440
|
+
function dedupeRowSignature(row, colIndices, normalizers, transforms) {
|
|
3441
|
+
const values = colIndices.map((c, i) => {
|
|
3442
|
+
const v = row[c];
|
|
3443
|
+
const transform = transforms[i];
|
|
3444
|
+
return transform && v !== null ? transform(v) : v;
|
|
3445
|
+
});
|
|
3446
|
+
return serializeKey(values, normalizers);
|
|
3447
|
+
}
|
|
3225
3448
|
/**
|
|
3226
3449
|
* Throws CONSTRAINT on the first pair of `rows` that share a signature over `columnIndices`.
|
|
3227
3450
|
*
|
|
3228
|
-
* SQL NULL semantics:
|
|
3229
|
-
* such rows never collide. A partial `predicate` restricts the judged set to rows
|
|
3230
|
-
* unambiguously. Shared by {@link StoreModule.validateUniqueOverExistingRows}
|
|
3231
|
-
* columns) and {@link StoreModule.validateUniqueIndexOverRows} (index columns).
|
|
3451
|
+
* SQL NULL semantics: {@link dedupeRowSignature} returns null when any constrained column is
|
|
3452
|
+
* NULL, and such rows never collide. A partial `predicate` restricts the judged set to rows
|
|
3453
|
+
* it accepts unambiguously. Shared by {@link StoreModule.validateUniqueOverExistingRows}
|
|
3454
|
+
* (constraint columns) and {@link StoreModule.validateUniqueIndexOverRows} (index columns).
|
|
3232
3455
|
*/
|
|
3233
3456
|
async function assertNoDuplicateRows(rows, tableSchema, columnIndices, normalizers, predicate) {
|
|
3457
|
+
const transforms = columnIndices.map(i => storeSemanticKeyTransform(tableSchema.columns[i]?.logicalType));
|
|
3234
3458
|
const seen = new Set();
|
|
3235
3459
|
for await (const row of rows) {
|
|
3236
3460
|
if (predicate && predicate.evaluate(row) !== true)
|
|
3237
3461
|
continue;
|
|
3238
|
-
const keySig =
|
|
3462
|
+
const keySig = dedupeRowSignature(row, columnIndices, normalizers, transforms);
|
|
3239
3463
|
if (keySig === null)
|
|
3240
3464
|
continue;
|
|
3241
3465
|
if (seen.has(keySig)) {
|