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
@@ -16,6 +16,13 @@ export declare class MongoSchemaGenerator extends AbstractDialect implements Sch
16
16
  generateCreateSchema(entities: readonly Type<unknown>[], options?: CreateSchemaOptions): string[];
17
17
  generateDropSchema(entities: readonly Type<unknown>[]): string[];
18
18
  private selected;
19
+ /**
20
+ * The indexes `@Field({ index })` declares, as the collection would hold them.
21
+ *
22
+ * One owner because two paths need it: creating a collection, and working out which of its indexes
23
+ * are missing. Derived twice, they drifted the moment either changed how a name is settled.
24
+ */
25
+ private fieldIndexes;
19
26
  generateCreateTable<E>(entity: Type<E>, _options?: {
20
27
  ifNotExists?: boolean;
21
28
  }): string[];
@@ -32,7 +39,7 @@ export declare class MongoSchemaGenerator extends AbstractDialect implements Sch
32
39
  */
33
40
  generateCreateIndex(tableName: string, index: IndexSchema): string;
34
41
  generateDropIndex(tableName: string, indexName: string): string;
35
- getSqlType(fieldOptions: FieldOptions, fieldType?: unknown): string;
42
+ getSqlType(fieldOptions: FieldOptions): string;
36
43
  generateCreateTableFromNode(table: TableNode, _options?: {
37
44
  ifNotExists?: boolean;
38
45
  }): string[];
@@ -32,22 +32,32 @@ export class MongoSchemaGenerator extends AbstractDialect {
32
32
  const wanted = new Set(only);
33
33
  return entities.filter((entity) => wanted.has(this.resolveTableName(getMeta(entity))));
34
34
  }
35
- generateCreateTable(entity, _options) {
36
- const meta = getMeta(entity);
37
- const collectionName = this.resolveTableName(meta);
38
- const indexes = [];
39
- for (const key of getKeys(meta.fields)) {
35
+ /**
36
+ * The indexes `@Field({ index })` declares, as the collection would hold them.
37
+ *
38
+ * One owner because two paths need it: creating a collection, and working out which of its indexes
39
+ * are missing. Derived twice, they drifted the moment either changed how a name is settled.
40
+ */
41
+ fieldIndexes(meta, collectionName) {
42
+ return getKeys(meta.fields).flatMap((key) => {
40
43
  const field = meta.fields[key];
41
- if (field?.index) {
42
- const columnName = this.resolveColumnName(key, field);
43
- const indexName = typeof field.index === 'string' ? field.index : derivedIndexName(collectionName, [columnName]);
44
- indexes.push({
45
- name: indexName,
44
+ if (!field?.index) {
45
+ return [];
46
+ }
47
+ const columnName = this.resolveColumnName(key, field);
48
+ return [
49
+ {
50
+ name: typeof field.index === 'string' ? field.index : derivedIndexName(collectionName, [columnName]),
46
51
  entries: [{ column: columnName }],
47
52
  unique: !!field.unique,
48
- });
49
- }
50
- }
53
+ },
54
+ ];
55
+ });
56
+ }
57
+ generateCreateTable(entity, _options) {
58
+ const meta = getMeta(entity);
59
+ const collectionName = this.resolveTableName(meta);
60
+ const indexes = this.fieldIndexes(meta, collectionName);
51
61
  // One `createIndex` command each, mirroring the SQL generator's `[CREATE TABLE, ...CREATE INDEX]`,
52
62
  // so the key spec is built here and the migrator only executes it.
53
63
  return [
@@ -110,7 +120,7 @@ export class MongoSchemaGenerator extends AbstractDialect {
110
120
  name: indexName,
111
121
  });
112
122
  }
113
- getSqlType(fieldOptions, fieldType) {
123
+ getSqlType(fieldOptions) {
114
124
  return '';
115
125
  }
116
126
  generateCreateTableFromNode(table, _options) {
@@ -137,22 +147,8 @@ export class MongoSchemaGenerator extends AbstractDialect {
137
147
  if (!currentTable) {
138
148
  return { tableName: collectionName, type: 'create' };
139
149
  }
140
- const indexesToAdd = [];
141
150
  const existingIndexes = new Set(currentTable.indexes?.map((i) => i.name) ?? []);
142
- for (const key of getKeys(meta.fields)) {
143
- const field = meta.fields[key];
144
- if (field?.index) {
145
- const columnName = this.resolveColumnName(key, field);
146
- const indexName = typeof field.index === 'string' ? field.index : derivedIndexName(collectionName, [columnName]);
147
- if (!existingIndexes.has(indexName)) {
148
- indexesToAdd.push({
149
- name: indexName,
150
- entries: [{ column: columnName }],
151
- unique: !!field.unique,
152
- });
153
- }
154
- }
155
- }
151
+ const indexesToAdd = this.fieldIndexes(meta, collectionName).filter((index) => !existingIndexes.has(index.name));
156
152
  if (indexesToAdd.length === 0) {
157
153
  return undefined;
158
154
  }
@@ -60,7 +60,10 @@ export declare abstract class AbstractSqlSchemaIntrospector extends BaseSqlIntro
60
60
  protected getColumns(read: TableRowReader, tableName: string): Promise<ColumnSchema[]>;
61
61
  protected getIndexes(read: TableRowReader, tableName: string): Promise<IndexSchema[]>;
62
62
  protected getForeignKeys(read: TableRowReader, tableName: string): Promise<ForeignKeySchema[]>;
63
- protected getPrimaryKey(read: TableRowReader, tableName: string): Promise<string[] | undefined>;
63
+ protected getPrimaryKey(read: TableRowReader, tableName: string): Promise<{
64
+ columns?: string[];
65
+ name?: string;
66
+ }>;
64
67
  protected tableExistsParams(tableName: string): unknown[];
65
68
  protected getColumnsParams(tableName: string): unknown[];
66
69
  protected getIndexesParams(tableName: string): unknown[];
@@ -108,6 +111,11 @@ export declare abstract class AbstractSqlSchemaIntrospector extends BaseSqlIntro
108
111
  * overrides this.
109
112
  */
110
113
  protected mapPrimaryKeyResult(results: RawRow[]): string[] | undefined;
114
+ /**
115
+ * What the engine calls the key's constraint, where the query reported one. Only a `DROP` needs it,
116
+ * and only the reported name will do - see {@link TableSchema.primaryKeyName}.
117
+ */
118
+ protected mapPrimaryKeyName(results: RawRow[]): string | undefined;
111
119
  /** Parse default value string to appropriate type. */
112
120
  protected abstract parseDefaultValue(defaultValue: string | null): unknown;
113
121
  }
@@ -55,7 +55,8 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
55
55
  return {
56
56
  name: tableName,
57
57
  columns,
58
- primaryKey,
58
+ primaryKey: primaryKey.columns,
59
+ primaryKeyName: primaryKey.name,
59
60
  indexes,
60
61
  foreignKeys,
61
62
  };
@@ -100,7 +101,7 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
100
101
  }
101
102
  async getPrimaryKey(read, tableName) {
102
103
  const results = await read(this.getPrimaryKeyQuery(tableName), this.getPrimaryKeyParams(tableName));
103
- return this.mapPrimaryKeyResult(results);
104
+ return { columns: this.mapPrimaryKeyResult(results), name: this.mapPrimaryKeyName(results) };
104
105
  }
105
106
  tableExistsParams(tableName) {
106
107
  return [tableName];
@@ -162,6 +163,14 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
162
163
  const columns = results.map((row) => String(row['column_name']));
163
164
  return columns.length ? columns : undefined;
164
165
  }
166
+ /**
167
+ * What the engine calls the key's constraint, where the query reported one. Only a `DROP` needs it,
168
+ * and only the reported name will do - see {@link TableSchema.primaryKeyName}.
169
+ */
170
+ mapPrimaryKeyName(results) {
171
+ const name = results[0]?.['constraint_name'];
172
+ return name === undefined || name === null ? undefined : String(name);
173
+ }
165
174
  }
166
175
  /** A {@link TableRowReader} over one querier: the same statement is only ever sent once. */
167
176
  function createTableRowReader(querier) {
@@ -64,10 +64,13 @@ export class BaseSqlIntrospector {
64
64
  referencedBy: [],
65
65
  };
66
66
  columns.set(col.name, column);
67
- if (col.isPrimaryKey) {
68
- table.primaryKey.push(column);
69
- }
70
67
  }
68
+ // From the ordered list the query returned, not from the per-column flags: `(a, b)` is a
69
+ // different key from `(b, a)`, and a flag says only that a column is *in* the key. Falls back
70
+ // to the flags for an introspector that reports no key of its own.
71
+ const keyColumns = schema.primaryKey ?? schema.columns.filter((col) => col.isPrimaryKey).map((col) => col.name);
72
+ table.primaryKey.push(...keyColumns.flatMap((name) => columns.get(name) ?? []));
73
+ table.primaryKeyName = schema.primaryKeyName;
71
74
  tableNodes.set(schema.name, table);
72
75
  ast.addTable(table);
73
76
  }
@@ -160,7 +160,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
160
160
  }
161
161
  getPrimaryKeyQuery(_tableName) {
162
162
  return /*sql*/ `
163
- SELECT kcu.column_name
163
+ SELECT kcu.column_name, tc.constraint_name
164
164
  FROM information_schema.table_constraints tc
165
165
  JOIN information_schema.key_column_usage kcu
166
166
  ON tc.constraint_name = kcu.constraint_name
@@ -1,3 +1,4 @@
1
+ import { derivedForeignKeyName } from '../../util/sql.util.js';
1
2
  import { AbstractSqlSchemaIntrospector } from './abstractSqlSchemaIntrospector.js';
2
3
  /**
3
4
  * SQLite schema introspector
@@ -104,11 +105,14 @@ export class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
104
105
  existing.push(row);
105
106
  grouped.set(id, existing);
106
107
  }
107
- return Array.from(grouped.entries()).map(([id, rows]) => {
108
+ return Array.from(grouped.entries()).map(([, rows]) => {
108
109
  const first = rows[0];
110
+ const columns = rows.map((r) => r.from);
109
111
  return {
110
- name: `fk_${tableName}_${id}`,
111
- columns: rows.map((r) => r.from),
112
+ // `PRAGMA foreign_key_list` reports no name, so one is derived the same way the entity side
113
+ // derives it. Seeded from the columns, not the PRAGMA's row id, which nothing else knows.
114
+ name: derivedForeignKeyName(tableName, columns),
115
+ columns,
112
116
  referencedTable: first.table,
113
117
  referencedColumns: rows.map((r) => r.to),
114
118
  onDelete: this.normalizeReferentialAction(first.on_delete),
@@ -436,6 +436,12 @@ export class Migrator {
436
436
  this.logger.logSkippedMigration(`[AutoSync] Skipped altering ${filteredDiff.columnsToAlter.length} columns in table '${diff.tableName}': ${filteredDiff.columnsToAlter.map((c) => c.to.name).join(', ')} (safe mode active). Use a migration or { safe: false } to apply.`);
437
437
  delete filteredDiff.columnsToAlter;
438
438
  }
439
+ if (filteredDiff.primaryKey) {
440
+ // Rewriting a key drops a constraint and rebuilds an index over the whole table, and fails
441
+ // outright where the new columns are null on rows that already exist. Firmly not additive.
442
+ this.logger.logSkippedMigration(`[AutoSync] Skipped changing the primary key of '${diff.tableName}' from (${filteredDiff.primaryKey.from.join(', ')}) to (${filteredDiff.primaryKey.to.join(', ')}) (safe mode active). Use a migration or { safe: false } to apply.`);
443
+ delete filteredDiff.primaryKey;
444
+ }
439
445
  delete filteredDiff.indexesToDrop;
440
446
  delete filteredDiff.foreignKeysToDrop;
441
447
  }
@@ -1,5 +1,6 @@
1
1
  import { type AbstractDialect, AbstractSqlDialect } from '../dialect/index.js';
2
2
  import type { SchemaAST } from '../schema/schemaAST.js';
3
+ import { type DiffOptions } from '../schema/schemaASTDiffer.js';
3
4
  import type { CanonicalType, ColumnNode, ForeignKeyAction, IndexNode, TableNode } from '../schema/types.js';
4
5
  import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldMeta, FieldOptions, IndexSchema, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
5
6
  import type { FullColumnDefinition, TableDefinition, TableForeignKeyDefinition } from './builder/types.js';
@@ -25,11 +26,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
25
26
  /**
26
27
  * Primary key type for auto-increment integer IDs
27
28
  */
28
- protected get serialPrimaryKeyType(): string;
29
- /**
30
- * Convert FieldOptions to CanonicalType using the unified type system.
31
- */
32
- protected getCanonicalType(field: FieldOptions, fieldType?: unknown): CanonicalType;
29
+ protected get serialType(): string;
33
30
  protected canonicalTypeToSql(type: CanonicalType): string;
34
31
  /**
35
32
  * Every `CREATE TABLE` for `entities`, then their foreign keys.
@@ -66,13 +63,15 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
66
63
  */
67
64
  generateDropIndex(tableName: string, indexName: string, schema?: string): string;
68
65
  /**
69
- * Generate a column definition from a {@link ColumnSchema}, whose type is the engine's own spelling
70
- * and may already carry its precision (or even `PRIMARY KEY`, for a serial).
66
+ * A column definition from a {@link ColumnSchema}, whose type is already the engine's own spelling
67
+ * and may carry its own size.
68
+ *
69
+ * Kept apart from {@link generateColumnFromNode} rather than folded into it: a `ColumnSchema` has no
70
+ * `enum`, because introspection reads one back as a `CHECK` constraint and not as a property of the
71
+ * column, so only the node knows enough to emit that clause. Both spell the definition through
72
+ * {@link renderColumn}, which is the part that must not be written twice.
71
73
  */
72
- generateColumnDefinitionFromSchema(column: ColumnSchema, options?: {
73
- includePrimaryKey?: boolean;
74
- includeUnique?: boolean;
75
- }): string;
74
+ generateColumnDefinitionFromSchema(column: ColumnSchema): string;
76
75
  /**
77
76
  * The one place a column definition is spelled. Both callers reach it - the `ColumnSchema` path above
78
77
  * and the `ColumnNode` path below - because the clause order and the "no NOT NULL on a primary key"
@@ -82,7 +81,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
82
81
  /** ` DEFAULT <sql>`, or nothing where the column declares none. Empty rather than `DEFAULT NULL`
83
82
  * so an absent default stays absent - `defaultValue: null` is the way to ask for one. */
84
83
  private defaultClause;
85
- getSqlType(field: FieldMeta, fieldType?: unknown): string;
84
+ getSqlType(field: FieldMeta): string;
86
85
  /**
87
86
  * Generate ALTER COLUMN statements (database-specific)
88
87
  */
@@ -94,39 +93,30 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
94
93
  /**
95
94
  * Compare an entity with a database table node and return the differences.
96
95
  */
97
- diffSchema<E>(entity: Type<E>, currentTable: TableNode | undefined): SchemaDiff | undefined;
98
96
  /**
99
- * Indexes the entity declares that the table does not have, matched by name and built the same way
100
- * `CREATE TABLE` builds them, so adding an `@Index` to an entity already in the database is picked
101
- * up rather than waiting for the table to be created from scratch somewhere else.
97
+ * How this entity differs from the table the database reported, as the migrator's `SchemaDiff`.
102
98
  *
103
- * Only ever additive. An index the entity does not name is left alone: it may well have been
104
- * created deliberately outside the ORM, and dropping it is a decision for a reviewed migration.
105
- *
106
- * A vector index is one of these like any other: MariaDB's `CREATE VECTOR INDEX ... ON t (col)`
107
- * adds one to a table that already exists, which the inline `CREATE TABLE` form it also has cannot.
108
- */
109
- private missingIndexes;
110
- private columnNodeToSchema;
111
- /**
112
- * Convert field options to ColumnSchema. Both sides of a diff are the engine's SQL spelling: what it
113
- * would create for this field, against what it reported for the existing column.
99
+ * The comparison itself is {@link diffTable}, the same one drift detection runs, so the two can no
100
+ * longer disagree about what has changed. Only two things are this side's own: the entity becomes a
101
+ * table node first, and types are compared as the *engine* would store them - see `normalizeType`.
114
102
  */
115
- protected fieldToColumnSchema<E>(fieldKey: string, field: FieldOptions, meta: EntityMeta<E>): ColumnSchema;
103
+ diffSchema<E>(entity: Type<E>, currentTable: TableNode | undefined): SchemaDiff | undefined;
116
104
  /**
117
- * Check if two columns differ enough to require alteration
105
+ * What the shared differ needs from a dialect: a type as this engine would actually store it.
106
+ *
107
+ * `boolean` is `TINYINT(1)` on MySQL and `INTEGER` on SQLite, so two canonical types that differ on
108
+ * paper can be one column in the database. Round-tripping through the engine's own spelling is what
109
+ * stops every such column reporting an alteration on every sync.
118
110
  */
119
- protected columnsNeedAlteration(current: ColumnSchema, desired: ColumnSchema): boolean;
120
111
  /**
121
- * Whether two column types are the same *as this engine stores them*.
112
+ * Indexes the entity declares that the table does not already have, in any shape.
122
113
  *
123
- * Both sides are SQL spellings, parsed back to canonical so that `INT` and `INTEGER`, or `DATETIME`
124
- * and `TIMESTAMP`, do not read as a change. Do not "simplify" this into comparing the entity's
125
- * canonical type against the column's: several canonical types share one storage type per engine
126
- * (`boolean` is `TINYINT(1)` on MySQL and `INTEGER` on SQLite), so that comparison reports an
127
- * alteration for those columns on every single sync.
114
+ * Additive only: an index the entity does not name may well have been created deliberately outside
115
+ * the ORM, and dropping it is a decision for a reviewed migration.
128
116
  */
129
- protected isTypeEqual(current: ColumnSchema, desired: ColumnSchema): boolean;
117
+ private missingIndexes;
118
+ protected diffOptions(): DiffOptions;
119
+ private columnNodeToSchema;
130
120
  /**
131
121
  * Compare two default values for equality
132
122
  */
@@ -156,6 +146,22 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
156
146
  generateRenameColumnSql(tableName: string, oldName: string, newName: string): string;
157
147
  generateAddForeignKeySql(tableName: string, foreignKey: TableForeignKeyDefinition): string;
158
148
  generateDropForeignKeySql(tableName: string, constraintName: string): string;
149
+ /**
150
+ * `ALTER TABLE ... ADD CONSTRAINT <name> PRIMARY KEY (...)`, the other half of
151
+ * {@link generateDropPrimaryKeySql}. Refused where the engine cannot alter a key at all, by name,
152
+ * rather than emitting DDL it will reject.
153
+ */
154
+ generateAddPrimaryKeySql(tableName: string, columns: readonly string[], name?: string): string;
155
+ /**
156
+ * Drops whatever key the table has.
157
+ *
158
+ * `constraintName` has to be what the constraint is *actually* called: the name introspection
159
+ * reported for a key the database already had, or the derived one for a key this generator itself
160
+ * added, which is what reversing a migration drops. Guessing either way names nothing. MySQL takes
161
+ * no name at all - a table's key is always `PRIMARY` there.
162
+ */
163
+ generateDropPrimaryKeySql(tableName: string, constraintName?: string): string;
164
+ private assertPrimaryKeyAlterable;
159
165
  }
160
166
  /**
161
167
  * The entities as an AST, named the way `generator` names things.