@objectstack/driver-sql 7.2.1 → 7.4.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.
- package/dist/index.d.mts +55 -2
- package/dist/index.d.ts +55 -2
- package/dist/index.js +119 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +119 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// src/sql-driver.ts
|
|
2
2
|
import { StorageNameMapping } from "@objectstack/spec/system";
|
|
3
|
+
import { ExternalSchemaModeViolationError } from "@objectstack/spec/shared";
|
|
3
4
|
import knex from "knex";
|
|
4
5
|
import { nanoid } from "nanoid";
|
|
6
|
+
import { createHash } from "crypto";
|
|
5
7
|
var DEFAULT_ID_LENGTH = 16;
|
|
6
8
|
var SEQUENCES_TABLE = "_objectstack_sequences";
|
|
7
9
|
var GLOBAL_TENANT = "__global__";
|
|
@@ -58,8 +60,10 @@ var SqlDriver = class {
|
|
|
58
60
|
this.logger = {
|
|
59
61
|
warn: (msg, meta) => console.warn(msg, meta ?? "")
|
|
60
62
|
};
|
|
61
|
-
|
|
62
|
-
this.
|
|
63
|
+
const { schemaMode, ...knexConfig } = config;
|
|
64
|
+
this.schemaMode = schemaMode ?? "managed";
|
|
65
|
+
this.config = knexConfig;
|
|
66
|
+
this.knex = knex(knexConfig);
|
|
63
67
|
}
|
|
64
68
|
get supports() {
|
|
65
69
|
return {
|
|
@@ -102,7 +106,9 @@ var SqlDriver = class {
|
|
|
102
106
|
schemaSync: true,
|
|
103
107
|
batchSchemaSync: false,
|
|
104
108
|
migrations: false,
|
|
105
|
-
indexes
|
|
109
|
+
// Object-level declared `indexes` (incl. multi-column UNIQUE) are
|
|
110
|
+
// materialized during `initObjects` — see `syncDeclaredIndexes`.
|
|
111
|
+
indexes: true,
|
|
106
112
|
// Performance & Optimization
|
|
107
113
|
connectionPooling: true,
|
|
108
114
|
preparedStatements: true,
|
|
@@ -204,6 +210,19 @@ var SqlDriver = class {
|
|
|
204
210
|
}
|
|
205
211
|
return null;
|
|
206
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* DDL gate (ADR-0015 §5.1). Single choke-point asserting that
|
|
215
|
+
* schema-mutating DDL is only performed on a `managed` datasource.
|
|
216
|
+
* Federated datasources (`external` / `validate-only`) are guests in a
|
|
217
|
+
* database ObjectStack does not own and must never run DDL against.
|
|
218
|
+
*/
|
|
219
|
+
assertSchemaMutable(operation) {
|
|
220
|
+
if (this.schemaMode !== "managed") {
|
|
221
|
+
throw new ExternalSchemaModeViolationError(
|
|
222
|
+
`DDL operation '${operation}' is forbidden: datasource schemaMode='${this.schemaMode}'. ObjectStack never mutates the schema of an external database.`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
207
226
|
// ===================================
|
|
208
227
|
// Lifecycle
|
|
209
228
|
// ===================================
|
|
@@ -742,6 +761,7 @@ var SqlDriver = class {
|
|
|
742
761
|
await this.initObjects([{ ...objectDef, name: object }]);
|
|
743
762
|
}
|
|
744
763
|
async dropTable(object, _options) {
|
|
764
|
+
this.assertSchemaMutable("dropTable");
|
|
745
765
|
await this.knex.schema.dropTableIfExists(object);
|
|
746
766
|
}
|
|
747
767
|
/**
|
|
@@ -749,6 +769,7 @@ var SqlDriver = class {
|
|
|
749
769
|
*/
|
|
750
770
|
async initObjects(objects) {
|
|
751
771
|
var _a, _b;
|
|
772
|
+
this.assertSchemaMutable("initObjects");
|
|
752
773
|
await this.ensureDatabaseExists();
|
|
753
774
|
for (const obj of objects) {
|
|
754
775
|
const tableName = StorageNameMapping.resolveTableName(obj);
|
|
@@ -834,6 +855,101 @@ var SqlDriver = class {
|
|
|
834
855
|
}
|
|
835
856
|
});
|
|
836
857
|
}
|
|
858
|
+
const declaredIndexes = obj.indexes;
|
|
859
|
+
if (Array.isArray(declaredIndexes) && declaredIndexes.length > 0) {
|
|
860
|
+
const colInfo = await this.knex(tableName).columnInfo();
|
|
861
|
+
const physicalColumns = new Set(Object.keys(colInfo));
|
|
862
|
+
await this.syncDeclaredIndexes(tableName, declaredIndexes, physicalColumns);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Build a deterministic index name for a declared index so repeated
|
|
868
|
+
* `initObjects` runs converge on the same identifier (and can detect an
|
|
869
|
+
* already-materialized index by name). Long names are hash-suffixed to
|
|
870
|
+
* stay within the 63/64-char identifier limits of Postgres/MySQL.
|
|
871
|
+
*/
|
|
872
|
+
buildIndexName(tableName, fields, unique) {
|
|
873
|
+
const prefix = unique ? "uniq" : "idx";
|
|
874
|
+
const base = `${prefix}_${tableName}_${fields.join("_")}`;
|
|
875
|
+
const MAX = 60;
|
|
876
|
+
if (base.length <= MAX) return base;
|
|
877
|
+
const hash = createHash("sha1").update(base).digest("hex").slice(0, 8);
|
|
878
|
+
return `${`${prefix}_${tableName}`.slice(0, MAX - 9)}_${hash}`;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Read the names of indexes that already exist on a table, per dialect.
|
|
882
|
+
* Used to make declared-index sync idempotent across repeated runs.
|
|
883
|
+
* Failures are swallowed — at worst we attempt a create and absorb the
|
|
884
|
+
* "already exists" error in `syncDeclaredIndexes`.
|
|
885
|
+
*/
|
|
886
|
+
async getExistingIndexNames(tableName) {
|
|
887
|
+
const names = /* @__PURE__ */ new Set();
|
|
888
|
+
try {
|
|
889
|
+
if (this.isSqlite) {
|
|
890
|
+
const safe = tableName.replace(/[^a-zA-Z0-9_]/g, "");
|
|
891
|
+
const rows = await this.knex.raw(`PRAGMA index_list(${safe})`);
|
|
892
|
+
for (const r of rows) names.add(r.name);
|
|
893
|
+
} else if (this.isPostgres) {
|
|
894
|
+
const res = await this.knex.raw(
|
|
895
|
+
`SELECT indexname FROM pg_indexes WHERE schemaname = 'public' AND tablename = ?`,
|
|
896
|
+
[tableName]
|
|
897
|
+
);
|
|
898
|
+
for (const r of res.rows) names.add(r.indexname);
|
|
899
|
+
} else if (this.isMysql) {
|
|
900
|
+
const res = await this.knex.raw(
|
|
901
|
+
`SELECT INDEX_NAME FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?`,
|
|
902
|
+
[tableName]
|
|
903
|
+
);
|
|
904
|
+
for (const r of res[0]) names.add(r.INDEX_NAME);
|
|
905
|
+
}
|
|
906
|
+
} catch {
|
|
907
|
+
}
|
|
908
|
+
return names;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Materialize declared object-level indexes.
|
|
912
|
+
*
|
|
913
|
+
* - Multi-column and single-column indexes are both supported.
|
|
914
|
+
* - `unique: true` emits a UNIQUE index. NULL-distinct semantics are the
|
|
915
|
+
* default across SQLite/Postgres/MySQL, so multiple NULL rows remain
|
|
916
|
+
* allowed while non-NULL duplicates are rejected — matching the
|
|
917
|
+
* convergence-on-conflict pattern the messaging pipeline relies on.
|
|
918
|
+
* - Idempotent: indexes already present (by deterministic name) are
|
|
919
|
+
* skipped, and an "already exists" race is absorbed.
|
|
920
|
+
* - Indexes referencing a column that wasn't materialized (e.g. a virtual
|
|
921
|
+
* `formula` field) are skipped with a warning rather than failing sync.
|
|
922
|
+
*/
|
|
923
|
+
async syncDeclaredIndexes(tableName, indexes, physicalColumns) {
|
|
924
|
+
const existing = await this.getExistingIndexNames(tableName);
|
|
925
|
+
for (const idx of indexes) {
|
|
926
|
+
const fields = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string" && f.length > 0) : [];
|
|
927
|
+
if (fields.length === 0) continue;
|
|
928
|
+
const missing = fields.filter((f) => !physicalColumns.has(f));
|
|
929
|
+
if (missing.length > 0) {
|
|
930
|
+
this.logger.warn(
|
|
931
|
+
`[sql-driver] skipping declared index on "${tableName}" \u2014 column(s) not materialized: ${missing.join(", ")}`,
|
|
932
|
+
{ tableName, fields }
|
|
933
|
+
);
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
const unique = idx.unique === true;
|
|
937
|
+
const name = typeof idx.name === "string" && idx.name.trim() ? idx.name.trim() : this.buildIndexName(tableName, fields, unique);
|
|
938
|
+
if (existing.has(name)) continue;
|
|
939
|
+
try {
|
|
940
|
+
await this.knex.schema.alterTable(tableName, (table) => {
|
|
941
|
+
if (unique) {
|
|
942
|
+
table.unique(fields, { indexName: name });
|
|
943
|
+
} else {
|
|
944
|
+
table.index(fields, name);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
existing.add(name);
|
|
948
|
+
} catch (e) {
|
|
949
|
+
const msg = String(e?.message ?? e);
|
|
950
|
+
if (/already exists|duplicate key name|exists/i.test(msg)) continue;
|
|
951
|
+
throw e;
|
|
952
|
+
}
|
|
837
953
|
}
|
|
838
954
|
}
|
|
839
955
|
// ===================================
|