uql-orm 0.42.1 → 0.44.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 (54) hide show
  1. package/dist/browser/querier/httpQuerier.d.ts +6 -0
  2. package/dist/browser/querier/httpQuerier.js +1 -1
  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/abstractDialect.js +5 -6
  6. package/dist/dialect/abstractSqlDialect.d.ts +10 -12
  7. package/dist/dialect/abstractSqlDialect.js +34 -34
  8. package/dist/dialect/jsonSql.d.ts +3 -2
  9. package/dist/dialect/jsonSql.js +7 -5
  10. package/dist/dialect/vectorCast.d.ts +0 -6
  11. package/dist/dialect/vectorCast.js +0 -8
  12. package/dist/entity/decorator/bag.d.ts +3 -0
  13. package/dist/entity/decorator/members.d.ts +5 -2
  14. package/dist/entity/index.d.ts +1 -1
  15. package/dist/entity/index.js +1 -1
  16. package/dist/entity/metadata/definition.d.ts +15 -13
  17. package/dist/entity/metadata/definition.js +73 -25
  18. package/dist/http/handler.d.ts +8 -0
  19. package/dist/http/handler.js +5 -5
  20. package/dist/maria/mariaDialect.js +2 -2
  21. package/dist/migrate/cli.d.ts +5 -0
  22. package/dist/migrate/cli.js +33 -16
  23. package/dist/migrate/codegen/entityTypes.d.ts +7 -0
  24. package/dist/migrate/codegen/entityTypes.js +69 -0
  25. package/dist/migrate/codegen/index.d.ts +1 -0
  26. package/dist/migrate/codegen/index.js +1 -0
  27. package/dist/migrate/drift/driftDetector.js +5 -1
  28. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
  29. package/dist/migrate/generator/mongoSchemaGenerator.js +1 -1
  30. package/dist/migrate/index.d.ts +1 -1
  31. package/dist/migrate/migrator.d.ts +34 -20
  32. package/dist/migrate/migrator.js +77 -34
  33. package/dist/migrate/schemaGenerator.d.ts +1 -5
  34. package/dist/migrate/schemaGenerator.js +11 -15
  35. package/dist/schema/canonicalType.d.ts +19 -4
  36. package/dist/schema/canonicalType.js +114 -164
  37. package/dist/schema/schemaASTBuilder.d.ts +23 -2
  38. package/dist/schema/schemaASTBuilder.js +2 -2
  39. package/dist/schema/schemaASTDiffer.js +6 -2
  40. package/dist/type/entity.d.ts +36 -6
  41. package/dist/type/migration.d.ts +14 -1
  42. package/dist/type/query.d.ts +2 -6
  43. package/dist/type/queryWhere.d.ts +13 -3
  44. package/dist/util/field.util.d.ts +19 -8
  45. package/dist/util/field.util.js +47 -51
  46. package/dist/util/fieldOption.util.d.ts +79 -0
  47. package/dist/util/fieldOption.util.js +84 -0
  48. package/dist/util/index.d.ts +1 -0
  49. package/dist/util/index.js +1 -0
  50. package/dist/util/object.util.d.ts +3 -3
  51. package/dist/util/object.util.js +3 -3
  52. package/dist/util/sql.util.d.ts +4 -0
  53. package/dist/util/sql.util.js +4 -0
  54. package/package.json +3 -3
@@ -6,6 +6,7 @@
6
6
  * - Canonical types (dialect-agnostic)
7
7
  * - TypeScript types (for entity generation)
8
8
  */
9
+ import { columnFamily } from '../util/field.util.js';
9
10
  /** Whether a category is one of the vector types, narrowing it to the cast pgvector names use. */
10
11
  export function isVectorCategory(category) {
11
12
  return category === 'vector' || category === 'halfvec' || category === 'sparsevec';
@@ -108,66 +109,57 @@ const PG_TYPE_MAP = {
108
109
  halfvec: 'HALFVEC',
109
110
  sparsevec: 'SPARSEVEC',
110
111
  };
112
+ const MYSQL_SCALAR_MAP = {
113
+ integer: 'INT',
114
+ float: 'FLOAT',
115
+ decimal: 'DECIMAL',
116
+ string: 'VARCHAR',
117
+ boolean: 'TINYINT(1)',
118
+ date: 'DATE',
119
+ time: 'TIME',
120
+ timestamp: 'DATETIME',
121
+ json: 'JSON',
122
+ uuid: 'CHAR(36)',
123
+ blob: 'BLOB',
124
+ };
125
+ const SQLITE_SCALAR_MAP = {
126
+ integer: 'INTEGER',
127
+ float: 'REAL',
128
+ decimal: 'REAL',
129
+ string: 'TEXT',
130
+ boolean: 'INTEGER',
131
+ date: 'TEXT',
132
+ time: 'TEXT',
133
+ timestamp: 'TEXT',
134
+ json: 'TEXT',
135
+ uuid: 'TEXT',
136
+ blob: 'BLOB',
137
+ };
138
+ /** MongoDB uses BSON types, not SQL types. These are placeholders for compatibility. */
139
+ const MONGO_SCALAR_MAP = {
140
+ integer: 'int',
141
+ float: 'double',
142
+ decimal: 'decimal128',
143
+ string: 'string',
144
+ boolean: 'bool',
145
+ date: 'date',
146
+ time: 'string',
147
+ timestamp: 'timestamp',
148
+ json: 'object',
149
+ uuid: 'binData',
150
+ blob: 'binData',
151
+ };
152
+ /** Every engine's scalars, and the one spelling it gives all three vector widths. */
111
153
  const CANONICAL_TO_SQL = {
112
154
  postgres: PG_TYPE_MAP,
113
155
  // CockroachDB's VECTOR is native, no extension needed.
114
156
  cockroachdb: withVectorType(PG_SCALAR_MAP, 'VECTOR'),
115
- mysql: withVectorType({
116
- integer: 'INT',
117
- float: 'FLOAT',
118
- decimal: 'DECIMAL',
119
- string: 'VARCHAR',
120
- boolean: 'TINYINT(1)',
121
- date: 'DATE',
122
- time: 'TIME',
123
- timestamp: 'DATETIME',
124
- json: 'JSON',
125
- uuid: 'CHAR(36)',
126
- blob: 'BLOB',
127
- },
128
157
  // MySQL does have a `VECTOR` type (26.7), but no distance function outside HeatWave and no vector
129
158
  // index, so JSON keeps the column queryable with the JSON operators and needs no conversion.
130
- 'JSON'),
131
- sqlite: withVectorType({
132
- integer: 'INTEGER',
133
- float: 'REAL',
134
- decimal: 'REAL',
135
- string: 'TEXT',
136
- boolean: 'INTEGER',
137
- date: 'TEXT',
138
- time: 'TEXT',
139
- timestamp: 'TEXT',
140
- json: 'TEXT',
141
- uuid: 'TEXT',
142
- blob: 'BLOB',
143
- }, 'TEXT'),
144
- mariadb: withVectorType({
145
- integer: 'INT',
146
- float: 'FLOAT',
147
- decimal: 'DECIMAL',
148
- string: 'VARCHAR',
149
- boolean: 'TINYINT(1)',
150
- date: 'DATE',
151
- time: 'TIME',
152
- timestamp: 'DATETIME',
153
- json: 'JSON',
154
- uuid: 'CHAR(36)',
155
- blob: 'BLOB',
156
- }, 'VECTOR'),
157
- // MongoDB uses BSON types, not SQL types. These are placeholders for compatibility.
158
- mongodb: withVectorType({
159
- integer: 'int',
160
- float: 'double',
161
- decimal: 'decimal128',
162
- string: 'string',
163
- boolean: 'bool',
164
- date: 'date',
165
- time: 'string',
166
- timestamp: 'timestamp',
167
- json: 'object',
168
- uuid: 'binData',
169
- blob: 'binData',
170
- }, 'array'),
159
+ mysql: withVectorType(MYSQL_SCALAR_MAP, 'JSON'),
160
+ mariadb: withVectorType(MYSQL_SCALAR_MAP, 'VECTOR'),
161
+ sqlite: withVectorType(SQLITE_SCALAR_MAP, 'TEXT'),
162
+ mongodb: withVectorType(MONGO_SCALAR_MAP, 'array'),
171
163
  };
172
164
  /**
173
165
  * Size variant modifiers for SQL types.
@@ -239,7 +231,7 @@ const CANONICAL_TO_TS = {
239
231
  timestamp: 'Date',
240
232
  json: 'unknown',
241
233
  uuid: 'string',
242
- blob: 'Buffer',
234
+ blob: 'Uint8Array',
243
235
  vector: 'number[]',
244
236
  halfvec: 'number[]',
245
237
  sparsevec: 'number[]',
@@ -387,136 +379,94 @@ function formatDecimalSqlType(type, dialect, baseType) {
387
379
  export function canonicalToTypeScript(type) {
388
380
  return CANONICAL_TO_TS[type.category];
389
381
  }
382
+ /**
383
+ * A type as `dialect` would actually store it: rendered to that engine's SQL and read back.
384
+ *
385
+ * Several canonical types share one storage type per engine - a `boolean` is `TINYINT(1)` on MySQL and
386
+ * `INTEGER` on SQLite - and only the engine settles an unstated bound, since `VARCHAR` is 255 on MySQL
387
+ * and `TEXT` on Postgres. Both paths that diff a schema compare through this, so a migration and a
388
+ * drift report cannot disagree about what changed.
389
+ */
390
+ export function engineType(dialect) {
391
+ return (type) => sqlToCanonical(canonicalToSql(type, dialect));
392
+ }
390
393
  /**
391
394
  * Convert UQL FieldOptions to a canonical type.
392
395
  */
393
- export function fieldOptionsToCanonical(options, tsType) {
394
- // If explicit columnType is specified, use it
396
+ export function fieldOptionsToCanonical(options) {
397
+ // An explicit column type is read exactly as an introspected one is: the SQL type, plus whatever
398
+ // bounds are stated beside it.
395
399
  if (options.columnType) {
396
- const base = sqlToCanonical(options.columnType);
397
- return {
398
- ...base,
399
- length: options.length ?? base.length,
400
- precision: options.precision ?? base.precision,
401
- scale: options.scale ?? base.scale,
402
- };
403
- }
404
- // Infer from type (could be constructor or string)
405
- const type = options.type || tsType;
406
- if (type === String) {
407
- return {
408
- category: 'string',
409
- length: options.length,
410
- };
411
- }
412
- if (type === Number) {
413
- if (options.precision || options.scale) {
414
- return {
415
- category: 'decimal',
416
- precision: options.precision,
417
- scale: options.scale,
418
- };
419
- }
420
- // BIGINT for every `Number`, key or not: a 32-bit column is a migration waiting to happen, and
421
- // the pools decode it back to a JS number at the wire (see `pgNumericTypes`).
422
- return { category: 'integer', size: 'big' };
423
- }
424
- if (type === Boolean) {
425
- return { category: 'boolean' };
426
- }
427
- if (type === Date) {
428
- return { category: 'timestamp' };
429
- }
430
- if (type === BigInt) {
431
- return { category: 'integer', size: 'big' };
400
+ return canonicalColumnType(options.columnType, options);
432
401
  }
402
+ // Infer from type, which is a SQL type string or one of the constructors.
403
+ const { type } = options;
433
404
  if (typeof type === 'string') {
434
405
  const canonical = sqlToCanonical(type);
435
406
  // Propagate explicit dimensions into CanonicalType.length for vector types
436
- if (options.dimensions && isVectorCategory(canonical.category)) {
437
- return { ...canonical, length: options.dimensions };
438
- }
439
- return canonical;
407
+ return options.dimensions && isVectorCategory(canonical.category)
408
+ ? { ...canonical, length: options.dimensions }
409
+ : canonical;
410
+ }
411
+ switch (columnFamily(type)) {
412
+ case 'numeric':
413
+ // BIGINT for every `Number` without a scale, key or not: a 32-bit column is a migration waiting
414
+ // to happen, and the pools decode it back to a JS number at the wire (see `pgNumericTypes`).
415
+ return type === Number && (options.precision || options.scale)
416
+ ? { category: 'decimal', precision: options.precision, scale: options.scale }
417
+ : { category: 'integer', size: 'big' };
418
+ case 'boolean':
419
+ return { category: 'boolean' };
420
+ case 'date':
421
+ return { category: 'timestamp' };
422
+ // `String`, and anything a reflected type left unrecognised.
423
+ default:
424
+ return { category: 'string', length: options.length };
440
425
  }
441
- // Default to string
442
- return { category: 'string', length: options.length };
443
426
  }
444
427
  /**
445
- * Compare two canonical types for equality.
446
- * Used for schema diffing.
428
+ * Compare two canonical types for equality. Used for schema diffing.
429
+ *
430
+ * Nothing here guesses an engine's own default for an unstated bound: `VARCHAR` is 255 on MySQL and
431
+ * `TEXT` on Postgres, and a comparison that assumed either was blind to that difference on the other.
432
+ * `DiffOptions.normalizeType` is what settles it, by putting both sides through the engine first.
447
433
  */
448
434
  export function areTypesEqual(a, b) {
449
- // Category must match
450
- if (a.category !== b.category) {
451
- return false;
452
- }
453
- // Size must match (if specified)
454
- if (a.size !== b.size) {
455
- return false;
456
- }
457
- // For strings, compare length (allowing for default 255)
458
- if (a.category === 'string') {
459
- const lengthA = a.length ?? 255;
460
- const lengthB = b.length ?? 255;
461
- if (lengthA !== lengthB) {
462
- return false;
463
- }
464
- }
465
- // For decimals, compare precision and scale (allowing for default 10, 2)
466
- if (a.category === 'decimal') {
467
- const pA = a.precision ?? 10;
468
- const pB = b.precision ?? 10;
469
- const sA = a.scale ?? 2;
470
- const sB = b.scale ?? 2;
471
- if (pA !== pB || sA !== sB) {
472
- return false;
473
- }
474
- }
475
- // For timestamps, compare timezone
476
- if (a.category === 'timestamp') {
477
- if (!!a.withTimezone !== !!b.withTimezone) {
478
- return false;
479
- }
480
- }
481
- // Unsigned must match
482
- if (!!a.unsigned !== !!b.unsigned) {
483
- return false;
484
- }
485
- return true;
435
+ return (a.category === b.category &&
436
+ a.size === b.size &&
437
+ a.length === b.length &&
438
+ a.precision === b.precision &&
439
+ a.scale === b.scale &&
440
+ !!a.withTimezone === !!b.withTimezone &&
441
+ !!a.unsigned === !!b.unsigned);
486
442
  }
487
443
  /**
488
- * Check if changing from type A to type B could cause data loss.
444
+ * Whether changing a column from one type to the other can lose what it holds.
445
+ *
446
+ * An unstated bound is the engine's widest, so stating one for the first time narrows the column just
447
+ * as lowering one does: `TEXT` to `VARCHAR(50)` truncates, and `NUMERIC` to `NUMERIC(5,2)` rounds.
489
448
  */
490
449
  export function isBreakingTypeChange(from, to) {
491
- // Changing category is always potentially breaking
492
450
  if (from.category !== to.category) {
493
451
  return true;
494
452
  }
495
- // Reducing size is breaking
496
- const sizeOrder = ['tiny', 'small', 'medium', 'big'];
497
- if (from.size && to.size) {
498
- const fromIndex = sizeOrder.indexOf(from.size);
499
- const toIndex = sizeOrder.indexOf(to.size);
500
- if (toIndex < fromIndex) {
501
- return true;
502
- }
503
- }
504
- // Reducing string length is breaking
505
- if (from.category === 'string' && from.length && to.length) {
506
- if (to.length < from.length) {
507
- return true;
508
- }
509
- }
510
- // Reducing precision/scale is breaking
511
- if (from.category === 'decimal') {
512
- if (from.precision && to.precision && to.precision < from.precision) {
513
- return true;
514
- }
515
- if (from.scale && to.scale && to.scale < from.scale) {
516
- return true;
517
- }
453
+ // Only where both state a size: an unsized member of a family is the engine's base type, wider than
454
+ // `tiny` and narrower than `big`, and which of those it is depends on a family this does not know.
455
+ if (from.size && to.size && SIZE_ORDER.indexOf(to.size) < SIZE_ORDER.indexOf(from.size)) {
456
+ return true;
518
457
  }
519
- return false;
458
+ return (narrows(from.length, to.length) ||
459
+ narrows(from.precision, to.precision) ||
460
+ narrows(from.scale, to.scale) ||
461
+ // Either direction drops half the range: signed loses the top bit, unsigned the negatives.
462
+ !!from.unsigned !== !!to.unsigned ||
463
+ // An offset the column stops keeping cannot be recovered from what is left.
464
+ (!!from.withTimezone && !to.withTimezone));
465
+ }
466
+ const SIZE_ORDER = ['tiny', 'small', 'medium', 'big'];
467
+ /** A bound narrowed, where an unstated one is unbounded. */
468
+ function narrows(from, to) {
469
+ return to !== undefined && (from === undefined || to < from);
520
470
  }
521
471
  /**
522
472
  * Get the UQL ColumnType that best matches a canonical type.
@@ -5,10 +5,11 @@
5
5
  * - Entity metadata (decorator-based entities)
6
6
  * - Database introspection results (TableSchema[])
7
7
  */
8
- import type { EntityMeta, FieldOptions, Type } from '../type/index.js';
8
+ import type { EntityGetter } from '../type/entity.js';
9
+ import type { EntityMeta, FieldMeta, FieldOptions, Type } from '../type/index.js';
9
10
  import type { NamingStrategy } from '../type/namingStrategy.js';
10
11
  import { SchemaAST } from './schemaAST.js';
11
- import { type ForeignKeyAction } from './types.js';
12
+ import { type CanonicalType, type ForeignKeyAction } from './types.js';
12
13
  /**
13
14
  * Options for building SchemaAST from entities.
14
15
  */
@@ -31,3 +32,23 @@ export interface BuildSchemaASTOptions {
31
32
  * resolves against a table another entity declares, and an index against the columns of its own.
32
33
  */
33
34
  export declare function buildSchemaAST(entities: readonly Type<unknown>[], options?: BuildSchemaASTOptions): SchemaAST;
35
+ /**
36
+ * Resolve the canonical type for a field, inheriting from the referenced
37
+ * entity's primary key when the field is a foreign-key reference
38
+ * (`@Field({ references: () => SomeEntity })`) with no explicit type of its
39
+ * own.
40
+ *
41
+ * Without this, a field like `creatorId?: UUID` (a bare TypeScript alias for
42
+ * `string`, erased at runtime) falls back to the generic string inference in
43
+ * {@link fieldOptionsToCanonical} and gets typed as TEXT/VARCHAR - producing a
44
+ * foreign key column whose type doesn't match the UUID primary key it
45
+ * references, which Postgres (and most databases) reject outright.
46
+ *
47
+ * `field.typeFromReference` (set by `defineField`, see entity/metadata/definition.ts)
48
+ * is what distinguishes "no type was given" from "the decorator explicitly set
49
+ * a type" - including explicit constructor overrides like `type: BigInt`, which
50
+ * a value-based check (e.g. `typeof field.type === 'string'`) would miss since
51
+ * reflection also produces constructor values like `String`/`Number`.
52
+ * `columnType` remains the unambiguous, always-respected explicit override.
53
+ */
54
+ export declare function resolveColumnCanonicalType(field: FieldMeta, seen?: Set<EntityGetter>): CanonicalType;
@@ -53,7 +53,7 @@ export function buildSchemaAST(entities, options = {}) {
53
53
  * reflection also produces constructor values like `String`/`Number`.
54
54
  * `columnType` remains the unambiguous, always-respected explicit override.
55
55
  */
56
- function resolveColumnCanonicalType(field, seen = new Set()) {
56
+ export function resolveColumnCanonicalType(field, seen = new Set()) {
57
57
  const hasExplicitType = !!field.columnType || !field.typeFromReference;
58
58
  if (!hasExplicitType && field.references && !seen.has(field.references)) {
59
59
  seen.add(field.references);
@@ -65,7 +65,7 @@ function resolveColumnCanonicalType(field, seen = new Set()) {
65
65
  return resolveColumnCanonicalType(referencedIdField, seen);
66
66
  }
67
67
  }
68
- return fieldOptionsToCanonical(field, field.type);
68
+ return fieldOptionsToCanonical(field);
69
69
  }
70
70
  /**
71
71
  * Add a table from entity metadata.
@@ -175,7 +175,8 @@ function diffColumn(tableName, source, target, opts) {
175
175
  // key column" rule these two replace used to hide.
176
176
  const generatedType = source.isAutoIncrement && target.isAutoIncrement;
177
177
  const impliedNotNull = source.isPrimaryKey && target.isPrimaryKey;
178
- if (!generatedType && !areTypesEqual(opts.normalizeType(source.type), opts.normalizeType(target.type))) {
178
+ const typeChanged = !generatedType && !areTypesEqual(opts.normalizeType(source.type), opts.normalizeType(target.type));
179
+ if (typeChanged) {
179
180
  differences.push(`type: ${formatType(source.type)} → ${formatType(target.type)}`);
180
181
  }
181
182
  if (!impliedNotNull && source.nullable !== target.nullable) {
@@ -202,7 +203,10 @@ function diffColumn(tableName, source, target, opts) {
202
203
  type: 'alter',
203
204
  expected: source,
204
205
  actual: target,
205
- isBreaking: isBreakingTypeChange(target.type, source.type),
206
+ // Only the type this diff actually reports: a column altered for its default carries no data loss,
207
+ // and a generated key's type - never compared above - reads as unsigned against an entity that
208
+ // cannot say so.
209
+ isBreaking: typeChanged && isBreakingTypeChange(target.type, source.type),
206
210
  description: differences.join(', '),
207
211
  };
208
212
  }
@@ -49,6 +49,8 @@ export type RelationKey<E> = Exclude<Key<E>, FieldKey<E> | MethodKey<E>>;
49
49
  * inferring junk like `T = string`), while the marker key only exists on branded types.
50
50
  */
51
51
  type IsJson<T> = '__json' extends keyof T ? true : false;
52
+ /** Whether `T` is what a JSON column holds: the branded payload, or an array of them. */
53
+ type IsJsonColumn<T> = IsJson<T> extends true ? true : IsJson<NonNullable<Unpacked<T>>>;
52
54
  /** The payload `P` of a branded `Json<P>`, or `never` for any non-JSON type. */
53
55
  type UnwrapJson<T> = IsJson<T> extends true ? (T extends Json<infer P> ? P : never) : never;
54
56
  /**
@@ -276,7 +278,7 @@ export type FieldType = StringConstructor | NumberConstructor | BooleanConstruct
276
278
  * Both `Json<T>` and `Json<T>[]` have to be recognised, and the array check has to precede the scalar
277
279
  * arms so a `number[]` vector is not read as a `number`.
278
280
  */
279
- export type TypeFor<V, T = NonNullable<V>> = IsJson<T> extends true ? JsonColumnType : IsJson<NonNullable<Unpacked<T>>> extends true ? JsonColumnType : T extends readonly number[] ? VectorColumnType : T extends string ? StringConstructor | StringColumnType : T extends number ? NumberConstructor | NumericColumnType : T extends bigint ? BigIntConstructor | NumericColumnType : T extends boolean ? BooleanConstructor | BooleanColumnType : T extends Date ? DateConstructor | DateColumnType : T extends Uint8Array ? BlobColumnType : FieldType;
281
+ export type TypeFor<V, T = NonNullable<V>> = IsJsonColumn<T> extends true ? JsonColumnType : T extends readonly number[] ? VectorColumnType : T extends string ? StringConstructor | StringColumnType : T extends number ? NumberConstructor | NumericColumnType : T extends bigint ? BigIntConstructor | NumericColumnType : T extends boolean ? BooleanConstructor | BooleanColumnType : T extends Date ? DateConstructor | DateColumnType : T extends Uint8Array ? BlobColumnType : FieldType;
280
282
  /**
281
283
  * A field as the registry holds it: what the user authored, plus what registration worked out.
282
284
  *
@@ -384,11 +386,9 @@ export type FieldOptions<V = TsTypeOf<FieldType>> = {
384
386
  */
385
387
  readonly unique?: boolean;
386
388
  /**
387
- * The column's DDL default, rendered into `CREATE TABLE` by `formatDefaultValue` - not `V`, unlike
388
- * the generators above. A JSONB column defaults with the SQL literal it stores, `defaultValue: '{}'`,
389
- * which is a string whatever the field's TypeScript type is.
389
+ * The column's DDL default, rendered into `CREATE TABLE` by `formatDefaultValue`.
390
390
  */
391
- readonly defaultValue?: Scalar | Record<string, unknown>;
391
+ readonly defaultValue?: DdlDefault<V>;
392
392
  /**
393
393
  * Whether the column is auto-incrementing (for integer IDs).
394
394
  */
@@ -403,6 +403,17 @@ export type FieldOptions<V = TsTypeOf<FieldType>> = {
403
403
  readonly comment?: string;
404
404
  };
405
405
  export type OnFieldCallback<V = TsTypeOf<FieldType>> = V | QueryRaw | (() => V | QueryRaw);
406
+ /**
407
+ * What a column may default to: the value it holds, except on a JSON column, which defaults with the
408
+ * SQL literal it stores (`defaultValue: '{}'`) whatever the property's TypeScript type is. Opening
409
+ * that exception to every field is what let `@Field({ type: Number, defaultValue: 'hello' })` compile.
410
+ *
411
+ * The erased shape - `FieldOptions` with no field in mind - admits every column's default at once, or
412
+ * no `FieldOptions<V>` would be assignable to the one the registry and the dialects read.
413
+ */
414
+ type DdlDefault<V, T = NonNullable<V>> = IsJsonColumn<T> extends true ? JsonDdlDefault : [TsTypeOf<FieldType>] extends [T] ? JsonDdlDefault | T : T;
415
+ /** What a JSON column, and the field-less `FieldOptions`, may default to. */
416
+ type JsonDdlDefault = Scalar | Record<string, unknown>;
406
417
  /**
407
418
  * The TypeScript types a field may be declared as, given the `type` it registers: the inverse of
408
419
  * {@link TypeFor}.
@@ -730,7 +741,10 @@ export type EntityIndexMeta = {
730
741
  } & VectorIndexOptions & IndexTypeOptions;
731
742
  export type EntityMeta<E> = {
732
743
  readonly entity: Type<E>;
744
+ /** The table, which is the class's own name where the entity named none - see {@link derivedName}. */
733
745
  name?: string;
746
+ /** Whether {@link name} came from the class rather than from the author, so a naming strategy applies. */
747
+ derivedName?: boolean;
734
748
  /** Set only when the entity named one; unset defers to the pool where it is used. See `AbstractDialect.resolveSchema`. */
735
749
  schema?: string;
736
750
  /**
@@ -760,7 +774,13 @@ export type EntityMeta<E> = {
760
774
  checks?: CheckSchema[];
761
775
  /** Lifecycle hooks registered via @BeforeInsert, @AfterUpdate, etc. */
762
776
  hooks?: Partial<Record<HookEvent, HookRegistration[]>>;
763
- processed?: boolean;
777
+ /**
778
+ * Bumped by every `define*` call, so anything derived from this metadata can tell that it changed.
779
+ * A content type registered at runtime keeps adding to an entity that has already been read.
780
+ */
781
+ revision: number;
782
+ /** The revision `getMeta` last finalized, which is what makes finalizing idempotent and re-entrant. */
783
+ processedAt?: number;
764
784
  };
765
785
  /**
766
786
  * Configurable options for an entity (`@Entity()` / `defineEntity`).
@@ -777,6 +797,16 @@ export type CheckOptions = {
777
797
  readonly name?: string;
778
798
  readonly expression: QueryRaw;
779
799
  };
800
+ /**
801
+ * An entity's members as the registry takes them, keyed by plain strings - what a decorator bag, an
802
+ * {@link EntityOptions} and a decorator bag both reduce to before anything is registered: a member
803
+ * decorator has no class to key against, so by then the keys are plain strings either way.
804
+ */
805
+ export type EntityMembers = {
806
+ readonly fields?: Readonly<Record<string, FieldOptions | undefined>>;
807
+ readonly relations?: Readonly<Record<string, RelationOptions | undefined>>;
808
+ readonly hooks?: Readonly<Partial<Record<HookEvent, readonly string[]>>>;
809
+ };
780
810
  export type EntityOptions<E = unknown> = {
781
811
  readonly name?: string;
782
812
  /**
@@ -206,6 +206,19 @@ export interface SchemaDiff {
206
206
  readonly foreignKeysToAdd?: ForeignKeySchema[];
207
207
  readonly foreignKeysToDrop?: string[];
208
208
  }
209
+ /**
210
+ * What every sync entry point takes: `safe` keeps it additive, `drop` lets it remove a column, and
211
+ * `logging` reports each statement. A plan ignores `logging`, having nothing to run.
212
+ */
213
+ export interface SyncOptions {
214
+ readonly safe?: boolean;
215
+ readonly drop?: boolean;
216
+ readonly logging?: boolean;
217
+ /** One entity instead of every registered one, for a schema that grows while the process runs. */
218
+ readonly entity?: Type<unknown>;
219
+ /** Drop every table and recreate it. Development only: it is the one option that loses data. */
220
+ readonly force?: boolean;
221
+ }
209
222
  export interface CreateSchemaOptions {
210
223
  readonly ifNotExists?: boolean;
211
224
  /**
@@ -262,7 +275,7 @@ export interface SchemaGenerator {
262
275
  /**
263
276
  * Get the SQL type for a field based on its options
264
277
  */
265
- getSqlType(fieldOptions: FieldOptions, fieldType?: unknown): string;
278
+ getSqlType(fieldOptions: FieldOptions): string;
266
279
  /**
267
280
  * Compare an entity with a database table node and return the differences.
268
281
  */
@@ -394,12 +394,8 @@ type CountedRelations<C extends PropertyKey> = [C] extends [never] ? unknown : {
394
394
  };
395
395
  };
396
396
  /** @internal */
397
- type QueryProjectedRow<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E>> = [S | X] extends [never] ? E : IsUniform<V> extends true ? [PopulatedToMany<E, P>] extends [never] ? {
398
- [K in keyof E as K extends ProjectedKeys<E, S, V, X, P, C> ? K : never]: E[K];
399
- } : // A populated to-many is always a list, empty where the parent has no children, so it maps
400
- {
401
- [K in keyof E as K extends Exclude<ProjectedKeys<E, S, V, X, P, C>, PopulatedToMany<E, P>> ? K : never]: E[K];
402
- } & {
397
+ type QueryProjectedRow<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E>> = [S | X] extends [never] ? E : IsUniform<V> extends true ? [PopulatedToMany<E, P>] extends [never] ? Pick<E, ProjectedKeys<E, S, V, X, P, C> & keyof E> : // A populated to-many is always a list, empty where the parent has no children, so it maps
398
+ Pick<E, Exclude<ProjectedKeys<E, S, V, X, P, C>, PopulatedToMany<E, P>> & keyof E> & {
403
399
  [K in PopulatedToMany<E, P>]-?: NonNullable<E[K]>;
404
400
  } : E;
405
401
  /** The to-many relations a query populated, which come back as lists rather than as optional ones. */
@@ -296,10 +296,20 @@ type QueryCommonOp = Exclude<keyof QueryWhereFieldOperatorMap<unknown>, QueryStr
296
296
  type QueryAllowedOp<T> = QueryCommonOp | ([NonNullable<T>] extends [QueryComparableScalar] ? QueryOrderedOp : never) | ([NonNullable<T>] extends [string] ? QueryStringOp : never) | ([NonNullable<T>] extends [readonly number[] | Uint8Array] ? QueryVectorOp : never) | (IsMany<T> extends true ? QueryArrayOp : never);
297
297
  /**
298
298
  * Operators applicable to a field of type `T`: string operators require string fields, ordering
299
- * operators comparable fields, array operators array fields. `unknown` stays fully permissive
300
- * (untyped JSON dot-paths, erased dialect shapes).
299
+ * operators comparable fields, array operators array fields.
300
+ *
301
+ * Two shapes stay fully permissive, because neither says anything to check against: `unknown`
302
+ * (untyped JSON dot-paths, erased dialect shapes), and a field typed as every scalar at once - the
303
+ * column of a content type defined at runtime. Narrowing to what they share would leave a dynamic
304
+ * row with equality alone, since no operator applies to a boolean and a blob both.
305
+ */
306
+ export type QueryWhereFieldOperators<T> = unknown extends T ? QueryWhereFieldOperatorMap<T> : IsUntypedColumn<T> extends true ? QueryWhereFieldOperatorMap<T> : Pick<QueryWhereFieldOperatorMap<T>, QueryAllowedOp<T>>;
307
+ /**
308
+ * Whether a column admits every scalar at once, which is what an entity keyed by an index signature
309
+ * says about all of its columns. `Scalar` is the yardstick rather than a parameter: the question is
310
+ * whether `T` is at least that wide, and nothing narrower than the whole union answers it.
301
311
  */
302
- export type QueryWhereFieldOperators<T> = unknown extends T ? QueryWhereFieldOperatorMap<T> : Pick<QueryWhereFieldOperatorMap<T>, QueryAllowedOp<T>>;
312
+ type IsUntypedColumn<T> = [Scalar] extends [NonNullable<T>] ? true : false;
303
313
  /**
304
314
  * Value for a field comparison. A bare array is an implicit `$in` for scalar fields only:
305
315
  * on array-typed fields (e.g. a vector `number[]`) an array of arrays is ambiguous, so
@@ -1,16 +1,27 @@
1
1
  import type { EntityMeta, FieldOptions } from '../type/index.js';
2
2
  /**
3
- * Checks if a field type is numeric (Number, BigInt, or explicit numeric logical types)
3
+ * The kind of column a field lands on, which is what decides whether an option means anything on it:
4
+ * `length` is a string's, `precision` a number's, `dimensions` a vector's. Named in the words an
5
+ * error reports it in, so there is no second table of labels to keep in step.
4
6
  */
5
- export declare function isNumericType(type: unknown): boolean;
7
+ export type ColumnFamily = 'string' | 'numeric' | 'boolean' | 'date' | 'json' | 'blob' | 'vector';
6
8
  /**
7
- * Checks if a field type is boolean (Boolean, or an explicit boolean logical type)
9
+ * The runtime half of the column-type unions in `type/entity.ts`, which TypeScript erases. Each list
10
+ * is checked against its own union, so a type cannot be filed under the wrong family, and
11
+ * {@link UnplacedColumnType} refuses to compile if a new one is filed under none. Nothing here
12
+ * restates the unions: the compile-time side of the same question reads them directly.
8
13
  */
9
- export declare function isBooleanType(type: unknown): boolean;
10
- /**
11
- * Checks if a field type is JSON
12
- */
13
- export declare function isJsonType(type: unknown): boolean;
14
+ export declare const COLUMN_TYPES_BY_FAMILY: {
15
+ readonly numeric: readonly ["int", "integer", "tinyint", "smallint", "bigint", "float", "float4", "float8", "double", "double precision", "decimal", "numeric", "real", "serial", "smallserial", "bigserial"];
16
+ readonly string: readonly ["char", "varchar", "text", "uuid"];
17
+ readonly date: readonly ["date", "time", "datetime", "timestamp", "timestamptz"];
18
+ readonly json: readonly ["json", "jsonb"];
19
+ readonly blob: readonly ["blob", "bytea"];
20
+ readonly boolean: readonly ["bool", "boolean"];
21
+ readonly vector: readonly ["vector", "halfvec", "sparsevec"];
22
+ };
23
+ /** The family of a logical field type, or `undefined` where it names none. */
24
+ export declare function columnFamily(type: unknown): ColumnFamily | undefined;
14
25
  /**
15
26
  * Whether the field is the entity's *whole* primary key - the only kind a serial can stand in for,
16
27
  * and the only one that may state `PRIMARY KEY` in its own column definition.