uql-orm 0.42.0 → 0.43.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 (61) hide show
  1. package/dist/browser/querier/httpQuerier.d.ts +3 -3
  2. package/dist/browser/type/clientQuerier.d.ts +3 -3
  3. package/dist/browser/uql-browser.min.js +2 -2
  4. package/dist/browser/uql-browser.min.js.map +5 -5
  5. package/dist/dialect/abstractSqlDialect.d.ts +41 -14
  6. package/dist/dialect/abstractSqlDialect.js +75 -44
  7. package/dist/dialect/jsonSql.d.ts +3 -2
  8. package/dist/dialect/jsonSql.js +7 -5
  9. package/dist/dialect/mysqlLikeSqlDialect.d.ts +2 -1
  10. package/dist/dialect/mysqlLikeSqlDialect.js +3 -1
  11. package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
  12. package/dist/dialect/pgLikeSqlDialect.js +2 -1
  13. package/dist/dialect/vectorCast.d.ts +0 -6
  14. package/dist/dialect/vectorCast.js +0 -8
  15. package/dist/entity/decorator/entity.d.ts +1 -1
  16. package/dist/entity/decorator/entity.js +1 -1
  17. package/dist/entity/decorator/members.d.ts +5 -2
  18. package/dist/entity/metadata/definition.js +29 -12
  19. package/dist/maria/mariaDialect.js +4 -3
  20. package/dist/migrate/builder/tableBuilder.js +5 -4
  21. package/dist/migrate/drift/driftDetector.js +21 -1
  22. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +8 -1
  23. package/dist/migrate/generator/mongoSchemaGenerator.js +25 -29
  24. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +9 -1
  25. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +11 -2
  26. package/dist/migrate/introspection/baseSqlIntrospector.js +6 -3
  27. package/dist/migrate/introspection/postgresIntrospector.js +1 -1
  28. package/dist/migrate/introspection/sqliteIntrospector.js +7 -3
  29. package/dist/migrate/migrator.js +6 -0
  30. package/dist/migrate/schemaGenerator.d.ts +43 -37
  31. package/dist/migrate/schemaGenerator.js +163 -150
  32. package/dist/mongo/mongoDialect.js +22 -8
  33. package/dist/postgres/postgresDialect.js +1 -1
  34. package/dist/querier/abstractQuerier.js +18 -7
  35. package/dist/querier/relationCount.js +9 -7
  36. package/dist/schema/canonicalType.d.ts +19 -4
  37. package/dist/schema/canonicalType.js +114 -164
  38. package/dist/schema/indexDifferences.d.ts +28 -0
  39. package/dist/schema/indexDifferences.js +46 -0
  40. package/dist/schema/schemaASTBuilder.js +27 -33
  41. package/dist/schema/schemaASTDiffer.d.ts +27 -1
  42. package/dist/schema/schemaASTDiffer.js +59 -19
  43. package/dist/schema/types.d.ts +46 -7
  44. package/dist/sqlite/sqliteDialect.d.ts +2 -1
  45. package/dist/sqlite/sqliteDialect.js +4 -1
  46. package/dist/type/dialect.d.ts +6 -0
  47. package/dist/type/entity.d.ts +23 -6
  48. package/dist/type/migration.d.ts +20 -1
  49. package/dist/type/query.d.ts +2 -6
  50. package/dist/util/field.util.d.ts +28 -7
  51. package/dist/util/field.util.js +56 -48
  52. package/dist/util/fieldOption.util.d.ts +79 -0
  53. package/dist/util/fieldOption.util.js +84 -0
  54. package/dist/util/index.d.ts +1 -0
  55. package/dist/util/index.js +1 -0
  56. package/dist/util/object.util.js +11 -3
  57. package/dist/util/relationQuery.util.d.ts +10 -0
  58. package/dist/util/relationQuery.util.js +22 -2
  59. package/dist/util/sql.util.d.ts +28 -7
  60. package/dist/util/sql.util.js +79 -10
  61. package/package.json +2 -2
@@ -1,15 +1,22 @@
1
1
  import { getMeta, soleIdOf } from '../entity/index.js';
2
2
  import { parseQueryLock, QueryRaw, RAW_ALIAS, RAW_VALUE, VECTOR_QUERY_KEYS, } from '../type/index.js';
3
- import { asSelectMap, assertNonNegativeInteger, buildQueryWhereAsMap, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getSoftDeleteValue, hasKeys, isBooleanType, isJsonType, isJsonUpdateOp, isNumericType, isOperatorMap, isOperatorObject, isOperatorOnlyObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationSize, parseSortByCount, populatesRelations, raw, someValue, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
3
+ import { asSelectMap, assertNonNegativeInteger, buildQueryWhereAsMap, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getSoftDeleteValue, hasKeys, columnFamily, isJsonUpdateOp, isOperatorMap, isOperatorObject, isOperatorOnlyObject, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationSize, parseSortByCount, populatesRelations, raw, someValue, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
4
4
  import { escapeAnsiSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
5
5
  import { COUNT_ALIAS, DISTINCT_DERIVED_ALIAS, JSON_ELEM_ALIAS_PREFIX } from './aliases.js';
6
6
  import { buildElemMatchConditions } from './jsonArrayElemMatchUtils.js';
7
7
  import { isJsonbOp, jsonCompareMode, jsonElemExists } from './jsonSql.js';
8
8
  import { SqlQueryContext } from './queryContext.js';
9
9
  import { NO_JOINS, resolveQueryJoins, resolveSortableJoin, } from './queryJoins.js';
10
- import { isVectorFieldType, resolveVectorCast } from './vectorCast.js';
10
+ import { resolveVectorCast } from './vectorCast.js';
11
11
  import { VectorSqlDialect } from './vectorSqlDialect.js';
12
12
  export class AbstractSqlDialect extends VectorSqlDialect {
13
+ /**
14
+ * Whether {@link serialType} states `PRIMARY KEY` itself, so the table must not state it again.
15
+ *
16
+ * True on SQLite alone, where `AUTOINCREMENT` is legal only in the exact phrase
17
+ * `INTEGER PRIMARY KEY AUTOINCREMENT` - the key cannot be lifted out of the column there.
18
+ */
19
+ serialDeclaresPrimaryKey = false;
13
20
  /**
14
21
  * How this engine declares a namespace, so a generated migration creates the schemas its tables
15
22
  * need before creating them. Only reached where {@link DialectFeatures.schemas} is on. MySQL and
@@ -22,6 +29,11 @@ export class AbstractSqlDialect extends VectorSqlDialect {
22
29
  alterColumnStrategy = 'single-statement';
23
30
  alterColumnSyntax = 'ALTER COLUMN';
24
31
  dropForeignKeySyntax = 'DROP CONSTRAINT';
32
+ /**
33
+ * `DROP CONSTRAINT <name>` where a primary key is a named constraint like any other; MySQL spells
34
+ * it `DROP PRIMARY KEY` and takes no name, since a table's key is always called `PRIMARY` there.
35
+ */
36
+ dropPrimaryKeySyntax = 'DROP CONSTRAINT';
25
37
  dropIndexSyntax = 'standalone';
26
38
  renameTableSyntax = 'alter-table';
27
39
  booleanLiteral = 'native';
@@ -90,9 +102,22 @@ export class AbstractSqlDialect extends VectorSqlDialect {
90
102
  placeholder(_index) {
91
103
  return '?';
92
104
  }
105
+ /**
106
+ * `RETURNING <id column> AS id`, or nothing at all for a composite key.
107
+ *
108
+ * The alias names one column, and every column of a composite came from the caller, so there is no
109
+ * id the statement could report that the payload does not already carry - the same "no id to give"
110
+ * a `firstId` dialect already answers with. Empty rather than a refusal, so an insert and an upsert
111
+ * of a composite row both run; `idOf(meta, row)` names such a row.
112
+ */
93
113
  returningId(meta) {
94
- const idName = this.columnOf(meta, soleIdOf(meta, 'returning an inserted id'));
95
- return `RETURNING ${this.escapeId(idName)} ${this.escapeId('id')}`;
114
+ const expression = this.returningIdExpression(meta);
115
+ return expression ? `RETURNING ${expression}` : '';
116
+ }
117
+ /** `<id column> AS id` on its own, for a statement composing a `RETURNING` list of several items. */
118
+ returningIdExpression(meta) {
119
+ const [idKey] = meta.ids;
120
+ return meta.ids.length === 1 ? `${this.escapeId(this.columnOf(meta, idKey))} ${this.escapeId('id')}` : '';
96
121
  }
97
122
  search(ctx, entity, q = {}, opts = {}, joins = NO_JOINS) {
98
123
  const meta = getMeta(entity);
@@ -965,12 +990,13 @@ export class AbstractSqlDialect extends VectorSqlDialect {
965
990
  insert(ctx, entity, payload, opts) {
966
991
  this.appendInsertValues(ctx, entity, payload);
967
992
  // Every engine whose ids come back from the statement itself wants the same clause, so it is
968
- // appended once here instead of in an identical `insert` override per dialect. A composite key
969
- // has nothing to ask for: the alias names one column, and every column of it came from the
970
- // caller, so there is no id the statement could report that the payload does not already carry.
971
- const meta = getMeta(entity);
972
- if (this.insertIdSource === 'returning' && meta.ids.length === 1) {
973
- ctx.append(` ${this.returningId(meta)}`);
993
+ // appended once here instead of in an identical `insert` override per dialect. `returningId` is
994
+ // empty on a composite key, which has no id to ask for.
995
+ if (this.insertIdSource === 'returning') {
996
+ const returning = this.returningId(getMeta(entity));
997
+ if (returning) {
998
+ ctx.append(` ${returning}`);
999
+ }
974
1000
  }
975
1001
  }
976
1002
  /**
@@ -1063,14 +1089,19 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1063
1089
  * {@link PgLikeSqlDialect} overrides this: `$N` placeholders make array order irrelevant, so it can
1064
1090
  * bind into the main context and skip the second one.
1065
1091
  */
1066
- upsert(ctx, entity, conflictPaths, payload, extraReturning = '') {
1092
+ upsert(ctx, entity, conflictPaths, payload,
1093
+ /** One more `RETURNING` item, as a bare expression: this joins the list and adds the keyword. */
1094
+ extraReturning = '') {
1067
1095
  const meta = getMeta(entity);
1068
1096
  const updateCtx = this.upsertUpdateBindsInPlace ? ctx : this.createContext();
1069
1097
  const update = this.getUpsertUpdateAssignments(updateCtx, meta, conflictPaths, payload, this.upsertExcluded);
1070
1098
  const keys = this.getUpsertConflictPathsStr(meta, conflictPaths);
1071
1099
  const onConflict = update ? `DO UPDATE SET ${update}` : 'DO NOTHING';
1100
+ // Composed rather than concatenated: a composite key contributes no id item, and a dialect's own
1101
+ // item (Postgres's `_created`) still has to be the *first* thing after the keyword when it is.
1102
+ const returning = [this.returningIdExpression(meta), extraReturning].filter(Boolean).join(', ');
1072
1103
  this.appendInsertValues(ctx, entity, payload);
1073
- ctx.append(` ON CONFLICT (${keys}) ${onConflict} ${this.returningId(meta)}${extraReturning}`);
1104
+ ctx.append(` ON CONFLICT (${keys}) ${onConflict}${returning ? ` RETURNING ${returning}` : ''}`);
1074
1105
  if (updateCtx !== ctx) {
1075
1106
  ctx.pushValue(...updateCtx.values);
1076
1107
  }
@@ -1156,22 +1187,19 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1156
1187
  }
1157
1188
  /**
1158
1189
  * How a column's values are written. A function of the column, not of the value, so a bulk insert
1159
- * classifies each column once instead of re-deciding per row: a 20-row, 6-column insert asked
1160
- * `isJsonType` and `isVectorFieldType` 120 times to get the same six answers.
1190
+ * classifies each column once instead of re-deciding per row: a 20-row, 6-column insert used to ask
1191
+ * 120 times to get the same six answers.
1161
1192
  */
1162
1193
  persistKind(field) {
1163
- const type = field?.type;
1164
- if (isJsonType(type)) {
1165
- return 'json';
1166
- }
1167
- return isVectorFieldType(type) ? 'vector' : 'plain';
1194
+ const family = columnFamily(field?.type);
1195
+ return family === 'json' || family === 'vector' ? family : 'plain';
1168
1196
  }
1169
1197
  /**
1170
1198
  * Which of an entity's columns need decoding on READ, and how: the inverse of {@link persistKind},
1171
1199
  * cached per entity for the same reason it classifies per column. A 1000-row read of a 10-field
1172
- * entity otherwise asks `isJsonType` (which lowercases a string on every call) 10,000 times to get
1173
- * the same ten answers. Most entities land here for their numeric columns alone, where the per-row
1174
- * cost is one `typeof` against a value the driver usually decoded already.
1200
+ * entity otherwise asks {@link columnFamily} (which lowercases a string on every call) 10,000 times
1201
+ * to get the same ten answers. Most entities land here for their numeric columns alone, where the
1202
+ * per-row cost is one `typeof` against a value the driver usually decoded already.
1175
1203
  *
1176
1204
  * Dialect-aware exactly like {@link supportedVectorType}, because it has to be: a `sparsevec` field
1177
1205
  * is written as a plain dense vector everywhere but Postgres, so reading it back by the field's own
@@ -1183,23 +1211,26 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1183
1211
  * the field says it was meant as a number; and `type: BigInt` shares BIGINT with `type: Number`, so
1184
1212
  * the wire decode has to be undone for it. All are no-ops where the driver already decoded.
1185
1213
  *
1186
- * Classified through the same `isNumericType`/`isBooleanType`/`isJsonType` the rest of the library
1187
- * uses, not against the constructors: `type` accepts a string logical type for every one of these
1188
- * (`@Field({ type: 'decimal' })`), and matching `=== Number` alone left those reading back as text.
1214
+ * Classified through the same {@link columnFamily} the rest of the library uses, not against the
1215
+ * constructors: `type` accepts a string logical type for every one of these (`@Field({ type:
1216
+ * 'decimal' })`), and matching `=== Number` alone left those reading back as text.
1189
1217
  */
1190
1218
  hydratableFields(entity) {
1219
+ const meta = getMeta(entity);
1191
1220
  const cached = this.hydratable.get(entity);
1192
- if (cached) {
1193
- return cached;
1221
+ // Against the revision, not merely present: a field added to an entity already read - a content
1222
+ // type the admin extended - would otherwise decode by the list its columns are missing from.
1223
+ if (cached?.[0] === meta.revision) {
1224
+ return cached[1];
1194
1225
  }
1195
1226
  const decoded = [];
1196
- for (const [key, field] of Object.entries(getMeta(entity).fields)) {
1227
+ for (const [key, field] of Object.entries(meta.fields)) {
1197
1228
  const kind = this.hydrateKind(field);
1198
1229
  if (kind) {
1199
1230
  decoded.push([key, kind]);
1200
1231
  }
1201
1232
  }
1202
- this.hydratable.set(entity, decoded);
1233
+ this.hydratable.set(entity, [meta.revision, decoded]);
1203
1234
  return decoded;
1204
1235
  }
1205
1236
  /**
@@ -1233,26 +1264,26 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1233
1264
  /**
1234
1265
  * The mirror of {@link persistKind}: what one column decodes as, or nothing if it needs no decode.
1235
1266
  *
1236
- * Ordered for correctness, not for speed - this runs once per entity, cached, never per row. The
1237
- * one order that is load-bearing is `BigInt` before {@link isNumericType}, which answers true for
1238
- * `BigInt` as well as `Number`: swap them and every `type: BigInt` property silently decodes to a
1239
- * JS number again.
1267
+ * `BigInt` is asked first because it shares the numeric family with `Number`: let the switch answer
1268
+ * it and every `type: BigInt` property silently decodes to a JS number again.
1240
1269
  */
1241
1270
  hydrateKind(field) {
1242
1271
  const type = field?.type;
1243
- if (isJsonType(type)) {
1244
- return 'json';
1245
- }
1246
- if (isVectorFieldType(type)) {
1247
- return this.supportedVectorType(resolveVectorCast(field));
1248
- }
1249
- if (isBooleanType(type)) {
1250
- return 'boolean';
1251
- }
1252
1272
  if (type === BigInt) {
1253
1273
  return 'bigint';
1254
1274
  }
1255
- return isNumericType(type) ? 'number' : undefined;
1275
+ switch (columnFamily(type)) {
1276
+ case 'json':
1277
+ return 'json';
1278
+ case 'vector':
1279
+ return this.supportedVectorType(resolveVectorCast(field));
1280
+ case 'boolean':
1281
+ return 'boolean';
1282
+ case 'numeric':
1283
+ return 'number';
1284
+ default:
1285
+ return undefined;
1286
+ }
1256
1287
  }
1257
1288
  hydratable = new WeakMap();
1258
1289
  /** The one type dispatch for a persisted value, over a column kind decided by the caller. */
@@ -1350,7 +1381,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1350
1381
  }
1351
1382
  const root = key.slice(0, dotIndex);
1352
1383
  const field = meta.fields[root];
1353
- if (!field || !isJsonType(field.type)) {
1384
+ if (!field || columnFamily(field.type) !== 'json') {
1354
1385
  return undefined;
1355
1386
  }
1356
1387
  const colName = this.resolveColumnName(root, field);
@@ -45,8 +45,9 @@ export declare function jsonCompareMode(value: unknown): JsonAccessMode;
45
45
  * operand. An index over a JSON path is only reachable by a comparison that extracts it the same way,
46
46
  * so the two have to answer alike - which is why they are one pair over one vocabulary.
47
47
  *
48
- * Reads the type through `util/field.util`'s predicates, which the dialects already carry: resolving
49
- * it through `schema/canonicalType` instead pulls that whole module into every consumer bundle.
48
+ * Reads the type through `util/field.util`'s own classifier, which the dialects already carry:
49
+ * resolving it through `schema/canonicalType` instead pulls that whole module into every consumer
50
+ * bundle.
50
51
  */
51
52
  export declare function jsonTypeMode(type: FieldType): JsonAccessMode;
52
53
  /**
@@ -1,4 +1,4 @@
1
- import { isBooleanType, isNumericType } from '../util/field.util.js';
1
+ import { columnFamily } from '../util/field.util.js';
2
2
  import { escapeSingleQuotes } from '../util/sqlLiteral.js';
3
3
  /**
4
4
  * A `'$.a.b'` JSON path literal, each dot-separated segment escaped. `suffix` appends an accessor
@@ -62,14 +62,16 @@ export function jsonCompareMode(value) {
62
62
  * operand. An index over a JSON path is only reachable by a comparison that extracts it the same way,
63
63
  * so the two have to answer alike - which is why they are one pair over one vocabulary.
64
64
  *
65
- * Reads the type through `util/field.util`'s predicates, which the dialects already carry: resolving
66
- * it through `schema/canonicalType` instead pulls that whole module into every consumer bundle.
65
+ * Reads the type through `util/field.util`'s own classifier, which the dialects already carry:
66
+ * resolving it through `schema/canonicalType` instead pulls that whole module into every consumer
67
+ * bundle.
67
68
  */
68
69
  export function jsonTypeMode(type) {
69
- if (isNumericType(type)) {
70
+ const family = columnFamily(type);
71
+ if (family === 'numeric') {
70
72
  return 'numeric';
71
73
  }
72
- return isBooleanType(type) ? 'json' : 'text';
74
+ return family === 'boolean' ? 'json' : 'text';
73
75
  }
74
76
  /**
75
77
  * Whether the operator reads the JSON *value* instead of its text form. The array operators always
@@ -24,7 +24,7 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
24
24
  estimatedCount<E>(ctx: QueryContext, entity: Type<E>): void;
25
25
  /** `OFFSET` is only legal after a `LIMIT` here, so a bare `$skip` needs one. */
26
26
  pager(ctx: QueryContext, opts: QueryPager): void;
27
- readonly serialPrimaryKey = "BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY";
27
+ readonly serialType = "BIGINT UNSIGNED AUTO_INCREMENT";
28
28
  readonly escapeIdChar = "`";
29
29
  readonly tableOptions = "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
30
30
  readonly beginTransactionCommand = "START TRANSACTION";
@@ -32,6 +32,7 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
32
32
  readonly rollbackTransactionCommand = "ROLLBACK";
33
33
  readonly isolationLevelStrategy = "set-before";
34
34
  readonly dropForeignKeySyntax = "DROP FOREIGN KEY";
35
+ readonly dropPrimaryKeySyntax = "DROP PRIMARY KEY";
35
36
  readonly dropIndexSyntax = "on-table";
36
37
  readonly renameTableSyntax = "rename-table";
37
38
  readonly alterColumnSyntax = "MODIFY COLUMN";
@@ -31,6 +31,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
31
31
  dropTableCascade: false,
32
32
  renameColumn: true,
33
33
  foreignKeyAlter: true,
34
+ primaryKeyAlter: true,
34
35
  columnComment: true,
35
36
  vectorIndexRequiresNotNull: false,
36
37
  vectorSupportsLength: false,
@@ -62,7 +63,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
62
63
  }
63
64
  super.pager(ctx, opts);
64
65
  }
65
- serialPrimaryKey = 'BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY';
66
+ serialType = 'BIGINT UNSIGNED AUTO_INCREMENT';
66
67
  escapeIdChar = '`';
67
68
  tableOptions = 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4';
68
69
  beginTransactionCommand = 'START TRANSACTION';
@@ -70,6 +71,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
70
71
  rollbackTransactionCommand = 'ROLLBACK';
71
72
  isolationLevelStrategy = 'set-before';
72
73
  dropForeignKeySyntax = 'DROP FOREIGN KEY';
74
+ dropPrimaryKeySyntax = 'DROP PRIMARY KEY';
73
75
  dropIndexSyntax = 'on-table';
74
76
  renameTableSyntax = 'rename-table';
75
77
  alterColumnSyntax = 'MODIFY COLUMN';
@@ -15,7 +15,7 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
15
15
  /** Default {@link DialectFeatures} for Postgres-wire dialects. */
16
16
  protected readonly featureDefaults: DialectFeatures;
17
17
  readonly escapeIdChar = "\"";
18
- readonly serialPrimaryKey: string;
18
+ readonly serialType: string;
19
19
  readonly tableOptions = "";
20
20
  readonly beginTransactionCommand = "BEGIN";
21
21
  readonly commitTransactionCommand = "COMMIT";
@@ -28,6 +28,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
28
28
  dropTableCascade: true,
29
29
  renameColumn: true,
30
30
  foreignKeyAlter: true,
31
+ primaryKeyAlter: true,
31
32
  columnComment: false,
32
33
  vectorIndexRequiresNotNull: false,
33
34
  vectorSupportsLength: true,
@@ -39,7 +40,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
39
40
  // under heavy concurrent insert load (writes concentrate on one range); this default still beats
40
41
  // `SERIAL` (CockroachDB's `unique_rowid()`, a ~64-bit value that overflows JS's safe-integer
41
42
  // range). High-throughput CockroachDB users should override this per-entity with a UUID PK.
42
- serialPrimaryKey = 'BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY';
43
+ serialType = 'BIGINT GENERATED BY DEFAULT AS IDENTITY';
43
44
  tableOptions = '';
44
45
  beginTransactionCommand = 'BEGIN';
45
46
  commitTransactionCommand = 'COMMIT';
@@ -4,12 +4,6 @@
4
4
  */
5
5
  /** Vector cast types supported by pgvector. */
6
6
  export type VectorCast = 'vector' | 'halfvec' | 'sparsevec';
7
- /**
8
- * Whether a declared field type is a vector of any width. Every dialect that treats vectors specially
9
- * has to answer this for all three, not just `vector`: matching that one alone left `halfvec` and
10
- * `sparsevec` fields binding as plain arrays on insert and reading back raw.
11
- */
12
- export declare function isVectorFieldType(type: unknown): boolean;
13
7
  /** Resolves the effective cast from field options, `columnType` taking priority over `type`. */
14
8
  export declare function resolveVectorCast(field: {
15
9
  type?: unknown;
@@ -2,14 +2,6 @@
2
2
  * Kept out of `schema/canonicalType.ts`: importing one function from that migration/codegen module
3
3
  * pulled all ~18 KB of its type-mapping tables into every consumer's bundle.
4
4
  */
5
- /**
6
- * Whether a declared field type is a vector of any width. Every dialect that treats vectors specially
7
- * has to answer this for all three, not just `vector`: matching that one alone left `halfvec` and
8
- * `sparsevec` fields binding as plain arrays on insert and reading back raw.
9
- */
10
- export function isVectorFieldType(type) {
11
- return type === 'vector' || type === 'halfvec' || type === 'sparsevec';
12
- }
13
5
  /** Resolves the effective cast from field options, `columnType` taking priority over `type`. */
14
6
  export function resolveVectorCast(field) {
15
7
  const raw = field?.columnType ?? field?.type;
@@ -21,7 +21,7 @@ export declare function Filter<E>(name: string, opts: FilterOptions<E>): (entity
21
21
  * `E` is inferred from the class the returned decorator is applied to, which is what lets the column
22
22
  * names be checked against it: `@Index(['nope'])` does not compile.
23
23
  *
24
- * @example `@Index(['lastName', 'firstName'], { name: 'idx_users_fullname' })`
24
+ * @example `@Index(['lastName', 'firstName'], { name: 'users_fullname_idx' })`
25
25
  * @example `@Index(['email'], { unique: true })`
26
26
  * @example `@Index(['status'], { where: "status = 'active'" })`
27
27
  */
@@ -33,7 +33,7 @@ export function Filter(name, opts) {
33
33
  * `E` is inferred from the class the returned decorator is applied to, which is what lets the column
34
34
  * names be checked against it: `@Index(['nope'])` does not compile.
35
35
  *
36
- * @example `@Index(['lastName', 'firstName'], { name: 'idx_users_fullname' })`
36
+ * @example `@Index(['lastName', 'firstName'], { name: 'users_fullname_idx' })`
37
37
  * @example `@Index(['email'], { unique: true })`
38
38
  * @example `@Index(['status'], { where: "status = 'active'" })`
39
39
  */
@@ -1,4 +1,5 @@
1
1
  import type { EntityGetter, FieldOptions, FieldType, IdValue, RelationManyToManyOptions, RelationManyToOneOptions, RelationOneToManyOptions, RelationOneToOneOptions, TsTypeOf } from '../../type/index.js';
2
+ import type { RejectIncompatible } from '../../util/index.js';
2
3
  /** A member decorator that also constrains the property it may be applied to. */
3
4
  type MemberDecorator<V> = (value: undefined, context: ClassFieldDecoratorContext<unknown, V>) => void;
4
5
  /**
@@ -51,7 +52,7 @@ export declare function Field<O extends FieldOptions<DeclaredValue<O>> & ({
51
52
  type: FieldType;
52
53
  } | {
53
54
  references: EntityGetter;
54
- }) & RejectUnknown<O, FieldOptions>>(opts: O): MemberDecorator<DeclaredValue<O> | undefined>;
55
+ }) & RejectUnknown<O, FieldOptions> & RejectIncompatible<O>>(opts: O): MemberDecorator<DeclaredValue<O> | undefined>;
55
56
  /**
56
57
  * Declares the primary key, checked the same way as `@Field`.
57
58
  *
@@ -60,7 +61,9 @@ export declare function Field<O extends FieldOptions<DeclaredValue<O>> & ({
60
61
  */
61
62
  export declare function Id<O extends FieldOptions<DeclaredValue<O>> & {
62
63
  type: FieldType;
63
- } & RejectUnknown<O, FieldOptions>>(opts: O): MemberDecorator<DeclaredValue<O> | undefined>;
64
+ } & RejectUnknown<O, FieldOptions> & RejectIncompatible<O> & {
65
+ readonly nullable?: false;
66
+ }>(opts: O): MemberDecorator<DeclaredValue<O> | undefined>;
64
67
  /**
65
68
  * `E` comes from the mandatory `entity` getter, so the context can insist the property really holds that
66
69
  * entity: `@ManyToOne({ entity: () => Other })` on a `Company` field stops compiling, and a to-many
@@ -1,5 +1,5 @@
1
1
  import { SOFT_DELETE_FILTER } from '../../type/index.js';
2
- import { getKeys, ddlText, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn, upperFirst, } from '../../util/index.js';
2
+ import { fieldOptionConflict, getKeys, ddlText, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn, upperFirst, } from '../../util/index.js';
3
3
  import { ownRegistrations } from '../decorator/bag.js';
4
4
  // Held on `globalThis` via the global symbol registry so a single metadata map survives multiple
5
5
  // evaluations of this module (HMR, duplicated/federated bundles, ESM+CJS dual-loading). Version-suffixed
@@ -10,11 +10,15 @@ const metaKey = Symbol.for('uql-orm/entity/metadata/v1');
10
10
  const metas = holder[metaKey] ?? new Map();
11
11
  holder[metaKey] = metas;
12
12
  export function defineField(entity, key, opts = {}) {
13
- const meta = ensureMeta(entity);
13
+ const meta = ensureWritableMeta(entity);
14
14
  if (!opts.type && !opts.references && !opts.virtual) {
15
15
  throw new TypeError(`'${entity.name}.${key}' needs a 'type'. Declare it - '@Field({ type: String })' - or point the field ` +
16
16
  "at another entity with 'references', which resolves the column type from its primary key.");
17
17
  }
18
+ const conflict = fieldOptionConflict(opts);
19
+ if (conflict) {
20
+ throw new TypeError(`'${entity.name}.${key}' ${conflict}.`);
21
+ }
18
22
  const fieldKey = key;
19
23
  // Flagged when the author gave `references` but no `type`, so schema generation knows to resolve the
20
24
  // column from the referenced primary key (picking up its `columnType`, length and chained keys)
@@ -32,7 +36,7 @@ export function defineRelation(entity, key, opts) {
32
36
  if (!opts.entity) {
33
37
  throw new TypeError(`'${entity.name}.${key}' needs an 'entity' getter, e.g. '@ManyToOne({ entity: () => Company })'.`);
34
38
  }
35
- const meta = ensureMeta(entity);
39
+ const meta = ensureWritableMeta(entity);
36
40
  // Registration writes the authored shape into a map declared as resolved: `getMeta` runs
37
41
  // `fillRelations`, which settles `entity`, `references` and `mappedBy` or throws. Bridging the two
38
42
  // shapes here is what lets every consumer read `RelationMeta` without asserting.
@@ -41,7 +45,7 @@ export function defineRelation(entity, key, opts) {
41
45
  return meta;
42
46
  }
43
47
  export function defineHook(entity, methodName, event) {
44
- const meta = ensureMeta(entity);
48
+ const meta = ensureWritableMeta(entity);
45
49
  if (!meta.hooks)
46
50
  meta.hooks = {};
47
51
  if (!meta.hooks[event])
@@ -54,7 +58,7 @@ export function defineHook(entity, methodName, event) {
54
58
  * lets the dialects render one shape instead of re-parsing it.
55
59
  */
56
60
  export function defineIndex(entity, index) {
57
- const meta = ensureMeta(entity);
61
+ const meta = ensureWritableMeta(entity);
58
62
  if (!meta.indexes)
59
63
  meta.indexes = [];
60
64
  meta.indexes.push({
@@ -66,7 +70,7 @@ export function defineIndex(entity, index) {
66
70
  return meta;
67
71
  }
68
72
  export function defineFilter(entity, name, opts) {
69
- const meta = ensureMeta(entity);
73
+ const meta = ensureWritableMeta(entity);
70
74
  if (name === SOFT_DELETE_FILTER) {
71
75
  throw TypeError(`'${entity.name}' filter name '${SOFT_DELETE_FILTER}' is reserved; it is auto-registered from @Field({ softDelete })`);
72
76
  }
@@ -112,7 +116,7 @@ export function defineEntity(entity, opts = {}) {
112
116
  throw new TypeError(`'${entity.name}' has a dotted name '${opts.name}'. Name the schema separately as ` +
113
117
  `{ schema: '${schema}', name: '${rest.join('.')}' }.`);
114
118
  }
115
- const meta = ensureMeta(entity);
119
+ const meta = ensureWritableMeta(entity);
116
120
  // Covers `defineEntity(Decorated)` called on a class whose members carry decorators. `@Entity()`
117
121
  // drains `context.metadata` itself, because TypeScript only attaches `Symbol.metadata` to the class
118
122
  // after class decorators return; draining empties the bag, so whichever runs second is a no-op.
@@ -132,8 +136,9 @@ export function defineEntity(entity, opts = {}) {
132
136
  if (!hasKeys(meta.fields)) {
133
137
  throw TypeError(`'${entity.name}' must have fields`);
134
138
  }
135
- meta.name = opts.name ?? entity.name;
136
- meta.schema = opts.schema;
139
+ // A later call composes onto the entity, so saying nothing about the table retracts nothing.
140
+ meta.name = opts.name ?? meta.name ?? entity.name;
141
+ meta.schema = opts.schema ?? meta.schema;
137
142
  let proto = Object.getPrototypeOf(entity.prototype);
138
143
  while (proto.constructor !== Object) {
139
144
  const parent = proto.constructor;
@@ -211,12 +216,21 @@ export function getEntities() {
211
216
  return acc;
212
217
  }, []);
213
218
  }
219
+ /**
220
+ * The metadata of `entity`, marked as changed. Every `define*` goes through this, and nothing outside
221
+ * this file writes to a meta, so it is the one place a derived cache can be told it has gone stale.
222
+ */
223
+ function ensureWritableMeta(entity) {
224
+ const meta = ensureMeta(entity);
225
+ meta.revision++;
226
+ return meta;
227
+ }
214
228
  function ensureMeta(entity) {
215
229
  let meta = metas.get(entity);
216
230
  if (meta) {
217
231
  return meta;
218
232
  }
219
- meta = { entity, ids: [], fields: {}, relations: {} };
233
+ meta = { entity, ids: [], fields: {}, relations: {}, revision: 0 };
220
234
  metas.set(entity, meta);
221
235
  return meta;
222
236
  }
@@ -225,10 +239,13 @@ export function getMeta(entity) {
225
239
  if (!meta) {
226
240
  throw TypeError(`'${entity.name}' is not an entity`);
227
241
  }
228
- if (meta.processed) {
242
+ if (meta.processedAt === meta.revision) {
229
243
  return meta;
230
244
  }
231
- meta.processed = true;
245
+ // Stamped before finalizing, not after: `fillInverseSide` reads the other side through `getMeta`,
246
+ // and with each side mapped by the other that recursion has to find this half-filled meta rather
247
+ // than run again. Finalizing twice is harmless anyway - every step of it skips what it settled.
248
+ meta.processedAt = meta.revision;
232
249
  return fillRelations(meta);
233
250
  }
234
251
  function fillRelations(meta) {
@@ -1,7 +1,7 @@
1
1
  import { jsonPath } from '../dialect/jsonSql.js';
2
2
  import { MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
3
- import { isVectorFieldType } from '../dialect/vectorCast.js';
4
3
  import { getMeta } from '../entity/index.js';
4
+ import { columnFamily } from '../util/field.util.js';
5
5
  import { MARIA_VECTOR_METRICS } from './mariaVectorMetrics.js';
6
6
  export class MariaDialect extends MysqlLikeSqlDialect {
7
7
  dialectName = 'mariadb';
@@ -19,7 +19,8 @@ export class MariaDialect extends MysqlLikeSqlDialect {
19
19
  indexIfNotExists: true,
20
20
  };
21
21
  upsertReturning(meta) {
22
- return ` ${this.returningId(meta)}`;
22
+ const returning = this.returningId(meta);
23
+ return returning ? ` ${returning}` : '';
23
24
  }
24
25
  /**
25
26
  * MariaDB supports neither MySQL's `->`/`->>` shorthand nor the base's chained form. `JSON_VALUE`
@@ -75,6 +76,6 @@ export class MariaDialect extends MysqlLikeSqlDialect {
75
76
  }
76
77
  /** The reverse: selecting a `VECTOR` column raw yields that blob, so it is read back as text. */
77
78
  selectFieldExpr(escapedColumn, field) {
78
- return isVectorFieldType(field.type) ? `VEC_ToText(${escapedColumn})` : escapedColumn;
79
+ return columnFamily(field.type) === 'vector' ? `VEC_ToText(${escapedColumn})` : escapedColumn;
79
80
  }
80
81
  }
@@ -154,10 +154,10 @@ export class TableBuilder {
154
154
  return this;
155
155
  }
156
156
  unique(columns, options) {
157
- return this.addIndex('uq', columns, options, true);
157
+ return this.addIndex(columns, options, true);
158
158
  }
159
159
  index(columns, options) {
160
- return this.addIndex('idx', columns, options, false);
160
+ return this.addIndex(columns, options, false);
161
161
  }
162
162
  /**
163
163
  * `@Index` and `table.index(...)` differ only in how the name is defaulted, so both normalize their
@@ -165,12 +165,13 @@ export class TableBuilder {
165
165
  * form, and anything left as a bare string would reach it as a column literally named `[object
166
166
  * Object]`.
167
167
  */
168
- addIndex(prefix, columns, options, unique) {
168
+ addIndex(columns, options, unique) {
169
169
  const { name, ...rest } = typeof options === 'string' ? { name: options } : (options ?? {});
170
170
  const entries = columns.map(normalizeIndexColumn);
171
171
  this._indexes.push({
172
172
  ...rest,
173
- name: name ?? `${prefix}_${this._name}_${entries.map((entry) => entry.column).join('_')}`,
173
+ name: name ??
174
+ derivedIndexName(this._name, entries.map((entry) => entry.column), unique),
174
175
  where: ddlText(rest.where, 'a partial-index predicate'),
175
176
  entries,
176
177
  unique,
@@ -4,7 +4,7 @@
4
4
  * Detects schema drift between expected schema (from entities) and
5
5
  * actual database schema.
6
6
  */
7
- import { canonicalToSql } from '../../schema/canonicalType.js';
7
+ import { canonicalToSql, engineType } from '../../schema/canonicalType.js';
8
8
  import { diffSchemas } from '../../schema/schemaASTDiffer.js';
9
9
  function resolveOptions(options) {
10
10
  return {
@@ -24,16 +24,21 @@ function resolveOptions(options) {
24
24
  */
25
25
  export function detectDrift(expectedAST, actualAST, options = {}) {
26
26
  const opts = resolveOptions(options);
27
+ const { dialect } = opts;
27
28
  const diff = diffSchemas(expectedAST, actualAST, {
28
29
  compareIndexes: opts.checkIndexes,
29
30
  indexFacets: opts.indexFacets,
30
31
  compareRelationships: opts.checkForeignKeys,
31
32
  excludeTables: opts.excludeTables,
33
+ // Without a dialect there is no engine to compare through, and `formatType` below then reports no
34
+ // type drift at all.
35
+ ...(dialect && { normalizeType: engineType(dialect) }),
32
36
  });
33
37
  const drifts = [
34
38
  ...detectTableDrifts(diff),
35
39
  ...detectColumnDrifts(diff, opts),
36
40
  ...detectIndexDrifts(diff),
41
+ ...detectPrimaryKeyDrifts(diff),
37
42
  ...detectRelationshipDrifts(diff),
38
43
  ];
39
44
  return {
@@ -43,6 +48,21 @@ export function detectDrift(expectedAST, actualAST, options = {}) {
43
48
  generatedAt: new Date(),
44
49
  };
45
50
  }
51
+ /**
52
+ * A table whose key holds different columns than the entity declares.
53
+ *
54
+ * Critical, and reported as a `constraint_mismatch` like any other: rows the database will accept
55
+ * are not the rows the ORM believes are unique, so it addresses by a key nothing enforces.
56
+ */
57
+ function detectPrimaryKeyDrifts(diff) {
58
+ return diff.primaryKeyDiffs.map((pkDiff) => ({
59
+ type: 'constraint_mismatch',
60
+ severity: 'critical',
61
+ table: pkDiff.table,
62
+ details: `Primary key of "${pkDiff.table}" is (${pkDiff.actual.join(', ') || 'none'}) in the database but (${pkDiff.expected.join(', ') || 'none'}) in the entity`,
63
+ suggestion: 'Generate a migration to change the primary key',
64
+ }));
65
+ }
46
66
  /**
47
67
  * Detect table-level drifts (missing/unexpected tables).
48
68
  */