uql-orm 0.61.0 → 0.62.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 (43) hide show
  1. package/dist/browser/uql-browser.min.js.map +1 -1
  2. package/dist/dialect/abstractSqlDialect.d.ts +12 -7
  3. package/dist/dialect/abstractSqlDialect.js +51 -50
  4. package/dist/dialect/mysqlLikeSqlDialect.js +1 -1
  5. package/dist/dialect/pgLikeSqlDialect.d.ts +1 -0
  6. package/dist/dialect/pgLikeSqlDialect.js +8 -7
  7. package/dist/dialect/queryContext.d.ts +5 -6
  8. package/dist/dialect/queryContext.js +8 -8
  9. package/dist/entity/decorator/entity.d.ts +3 -3
  10. package/dist/entity/decorator/entity.js +2 -2
  11. package/dist/entity/decorator/members.d.ts +3 -2
  12. package/dist/entity/decorator/members.js +1 -0
  13. package/dist/entity/metadata/definition.js +12 -6
  14. package/dist/migrate/builder/migrationBuilder.js +3 -5
  15. package/dist/migrate/builder/tableBuilder.js +2 -4
  16. package/dist/migrate/builder/types.d.ts +11 -3
  17. package/dist/migrate/codegen/indexDecoratorSource.js +3 -1
  18. package/dist/migrate/generator/definitionToNode.d.ts +8 -5
  19. package/dist/migrate/generator/definitionToNode.js +13 -4
  20. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +2 -0
  21. package/dist/migrate/generator/mongoSchemaGenerator.js +6 -1
  22. package/dist/migrate/schemaGenerator.d.ts +5 -3
  23. package/dist/migrate/schemaGenerator.js +9 -2
  24. package/dist/mongo/mongoDialect.js +1 -1
  25. package/dist/mssql/mssqlDialect.js +3 -5
  26. package/dist/schema/schemaASTBuilder.d.ts +6 -1
  27. package/dist/schema/schemaASTBuilder.js +17 -16
  28. package/dist/type/dialect.d.ts +15 -6
  29. package/dist/type/entity.d.ts +94 -34
  30. package/dist/type/migration.d.ts +9 -2
  31. package/dist/type/query.d.ts +3 -3
  32. package/dist/type/queryLock.d.ts +7 -7
  33. package/dist/type/queryLock.js +8 -6
  34. package/dist/type/queryRaw.d.ts +22 -30
  35. package/dist/type/queryRaw.js +3 -9
  36. package/dist/util/ddlExpression.util.d.ts +8 -13
  37. package/dist/util/ddlExpression.util.js +17 -19
  38. package/dist/util/dialect.util.d.ts +1 -1
  39. package/dist/util/dialect.util.js +3 -4
  40. package/dist/util/field.util.d.ts +1 -1
  41. package/dist/util/raw.d.ts +19 -17
  42. package/dist/util/raw.js +43 -17
  43. package/package.json +1 -1
@@ -8,6 +8,7 @@
8
8
  export class SqlQueryContext {
9
9
  dialect;
10
10
  statement;
11
+ inlineValues;
11
12
  sqlChunks = [];
12
13
  params;
13
14
  tableAliases = new Set();
@@ -19,14 +20,16 @@ export class SqlQueryContext {
19
20
  * than needing to be reconciled after the fact.
20
21
  * @param statement The context this one renders a fragment of, which owns the claimed aliases: a
21
22
  * fragment is part of one statement, so its aliases have to be unique across the whole of it.
23
+ * @param inlineValues See {@link QueryContext.inlineValues}; a fragment takes its statement's.
22
24
  */
23
- constructor(dialect, params = [], statement) {
25
+ constructor(dialect, params = [], statement, inlineValues = false) {
24
26
  this.dialect = dialect;
25
27
  this.statement = statement;
28
+ this.inlineValues = inlineValues;
26
29
  this.params = params;
27
30
  }
28
31
  createFragment() {
29
- return new SqlQueryContext(this.dialect, this.params, this.statement ?? this);
32
+ return new SqlQueryContext(this.dialect, this.params, this.statement ?? this, this.inlineValues);
30
33
  }
31
34
  /**
32
35
  * Appends raw SQL string fragments to the query.
@@ -41,14 +44,11 @@ export class SqlQueryContext {
41
44
  return this;
42
45
  }
43
46
  /**
44
- * Adds a value to the query parameters and appends its corresponding placeholder to the SQL.
45
- * The placeholder format is determined by the dialect (e.g., '?' or '$1').
46
- *
47
- * @param value The value to be parameterized.
48
- * @returns The current context instance for method chaining.
47
+ * Appends the SQL the dialect writes for `value`: a placeholder for the value bound, or its literal
48
+ * where this context inlines values.
49
49
  */
50
50
  addValue(value) {
51
- this.sqlChunks.push(this.dialect.addValue(this.params, value));
51
+ this.sqlChunks.push(this.dialect.addValue(this, value));
52
52
  return this;
53
53
  }
54
54
  /**
@@ -12,7 +12,7 @@ export declare function Entity<E>(opts?: EntityOptions<E>): (entity: Type<E>, co
12
12
  /**
13
13
  * Registers a named `$where` filter, applied to every query unless bypassed via `QueryOptions.filters`.
14
14
  *
15
- * @example `@Filter('active', { condition: { status: 'active' }, default: false })`
15
+ * @example `@Filter('active', { where: { status: 'active' }, default: false })`
16
16
  */
17
17
  export declare function Filter<E>(name: string, opts: FilterOptions<E>): (entity: Type<E>) => void;
18
18
  /**
@@ -22,6 +22,6 @@ export declare function Filter<E>(name: string, opts: FilterOptions<E>): (entity
22
22
  *
23
23
  * @example `@Index((user) => [user.lastName, user.firstName], { name: 'users_fullname_idx' })`
24
24
  * @example `@Index((user) => [user.email], { unique: true })`
25
- * @example `@Index((user) => [user.status], { where: "status = 'active'" })`
25
+ * @example `@Index((user) => [user.email], { unique: true, where: { deletedAt: null } })`
26
26
  */
27
- export declare function Index<E>(columns: (keys: KeyMap<E>) => readonly IndexColumnInput<FieldKey<NoInfer<E>>, NoInfer<E>>[], options?: EntityIndexOptions<NoInfer<E>>): (entity: Type<E>) => void;
27
+ export declare function Index<E>(columns: (keys: KeyMap<E>) => readonly IndexColumnInput<FieldKey<E>, E>[], options?: EntityIndexOptions<E>): (entity: Type<E>) => void;
@@ -20,7 +20,7 @@ export function Entity(opts) {
20
20
  /**
21
21
  * Registers a named `$where` filter, applied to every query unless bypassed via `QueryOptions.filters`.
22
22
  *
23
- * @example `@Filter('active', { condition: { status: 'active' }, default: false })`
23
+ * @example `@Filter('active', { where: { status: 'active' }, default: false })`
24
24
  */
25
25
  export function Filter(name, opts) {
26
26
  return (entity) => {
@@ -34,7 +34,7 @@ export function Filter(name, opts) {
34
34
  *
35
35
  * @example `@Index((user) => [user.lastName, user.firstName], { name: 'users_fullname_idx' })`
36
36
  * @example `@Index((user) => [user.email], { unique: true })`
37
- * @example `@Index((user) => [user.status], { where: "status = 'active'" })`
37
+ * @example `@Index((user) => [user.email], { unique: true, where: { deletedAt: null } })`
38
38
  */
39
39
  export function Index(columns, options = {}) {
40
40
  return (entity) => {
@@ -40,12 +40,13 @@ type EnumValue<Members, Declared> = Declared extends Members ? {
40
40
  *
41
41
  * @example `@Field({ type: String }) name?: string;`
42
42
  * @example `@Field({ references: () => User }) userId?: string;` (where `User.id` is a `uuid`)
43
+ * @example `@Field({ type: Number, computed: (line) => raw`${line.qty} * ${line.price}` }) total?: number;`
43
44
  */
44
- export declare function Field<O extends FieldOptions<DeclaredValue<O>> & ({
45
+ export declare function Field<This, O extends FieldOptions<DeclaredValue<O>, This> & ({
45
46
  type: FieldType;
46
47
  } | {
47
48
  references: EntityGetter;
48
- }) & RejectUnknown<O, FieldOptions> & RejectIncompatible<O>>(opts: O): MemberDecorator<DeclaredValue<O> | undefined>;
49
+ }) & RejectUnknown<O, FieldOptions> & RejectIncompatible<O>>(opts: O): MemberDecorator<DeclaredValue<O> | undefined, This>;
49
50
  /**
50
51
  * A key the type level cannot name, reported on each `@Id` that leaves it unnamed. Where no `idKey`
51
52
  * brand and no conventional name applies, `IdKey` falls back to every field, and `IdValue`,
@@ -8,6 +8,7 @@ import { memberRegistrations } from './bag.js';
8
8
  *
9
9
  * @example `@Field({ type: String }) name?: string;`
10
10
  * @example `@Field({ references: () => User }) userId?: string;` (where `User.id` is a `uuid`)
11
+ * @example `@Field({ type: Number, computed: (line) => raw`${line.qty} * ${line.price}` }) total?: number;`
11
12
  */
12
13
  export function Field(opts) {
13
14
  return (_value, context) => {
@@ -1,6 +1,6 @@
1
1
  import { SOFT_DELETE_FILTER } from '../../type/index.js';
2
2
  import { isInlinedExpression } from '../../util/field.util.js';
3
- import { entityName, fieldOptionConflict, getKeys, ddlText, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn, upperFirst, definedEntries, } from '../../util/index.js';
3
+ import { entityName, entitySql, entityWhere, fieldOptionConflict, getKeys, hasKeys, isToManyRelation, lowerFirst, normalizeIndexColumn, upperFirst, definedEntries, } from '../../util/index.js';
4
4
  import { ownRegistrations } from '../decorator/bag.js';
5
5
  /**
6
6
  * A map held on `globalThis` through the global symbol registry, so a single one survives multiple
@@ -31,8 +31,14 @@ export function defineField(entity, key, opts = {}) {
31
31
  // Flagged when the author gave `references` but no `type`, so schema generation knows to resolve the
32
32
  // column from the referenced primary key (picking up its `columnType`, length and chained keys)
33
33
  // instead of treating whatever ends up in `type` as deliberate.
34
- const resolved = opts.type ? opts : { ...opts, typeFromReference: true };
35
- meta.fields[fieldKey] = { ...meta.fields[fieldKey], name: key, ...resolved };
34
+ const { computed, ...rest } = opts;
35
+ const resolved = rest.type ? rest : { ...rest, typeFromReference: true };
36
+ meta.fields[fieldKey] = {
37
+ ...meta.fields[fieldKey],
38
+ name: key,
39
+ ...resolved,
40
+ ...(computed && { computed: entitySql(computed) }),
41
+ };
36
42
  return meta;
37
43
  }
38
44
  export function defineId(entity, key, opts) {
@@ -94,7 +100,7 @@ export function defineIndex(entity, index) {
94
100
  (meta.indexes ??= []).push({
95
101
  ...index,
96
102
  unique: index.unique ?? false,
97
- where: ddlText(index.where, 'a partial-index predicate'),
103
+ where: index.where && entityWhere(index.where),
98
104
  columns: index.columns(keys).map(normalizeIndexColumn),
99
105
  include: index.include?.(keys),
100
106
  });
@@ -161,7 +167,7 @@ export function defineEntity(entity, opts = {}) {
161
167
  });
162
168
  // Unnamed checks are named by the generator, as unnamed indexes are.
163
169
  for (const check of opts.checks ?? []) {
164
- (meta.checks ??= []).push({ name: check.name, expression: ddlText(check.expression, 'a check constraint') });
170
+ (meta.checks ??= []).push({ name: check.name, where: entityWhere(check.where) });
165
171
  }
166
172
  for (const index of opts.indexes ?? []) {
167
173
  defineIndex(entity, index);
@@ -204,7 +210,7 @@ export function defineEntity(entity, opts = {}) {
204
210
  meta.softDelete = softDeleteKeys[0];
205
211
  if (!meta.filters)
206
212
  meta.filters = {};
207
- meta.filters[SOFT_DELETE_FILTER] = { condition: { [meta.softDelete]: null }, default: true };
213
+ meta.filters[SOFT_DELETE_FILTER] = { where: { [meta.softDelete]: null }, default: true };
208
214
  }
209
215
  const ids = getIdKeys(meta);
210
216
  if (!ids.length) {
@@ -5,7 +5,7 @@
5
5
  * - OperationRecorder: Record operations only (for code generation)
6
6
  * - MigrationBuilder: Execute DDL operations (for integration tests/runtime)
7
7
  */
8
- import { ddlText, normalizeIndexColumn } from '../../util/index.js';
8
+ import { indexNameParts, normalizeIndexColumn } from '../../util/index.js';
9
9
  import { derivedIndexName } from '../../util/sql.util.js';
10
10
  import { createSchemaGenerator } from '../schemaGenerator.js';
11
11
  import { splitSqlStatements } from './splitSqlStatements.js';
@@ -23,9 +23,7 @@ function createIndexOperation(tableName, columns, options = {}) {
23
23
  tableName,
24
24
  index: {
25
25
  ...index,
26
- name: name ??
27
- derivedIndexName(tableName, entries.map((entry) => entry.column)),
28
- where: ddlText(index.where, 'a partial-index predicate'),
26
+ name: name ?? derivedIndexName(tableName, indexNameParts(entries)),
29
27
  entries,
30
28
  unique: unique ?? false,
31
29
  },
@@ -310,7 +308,7 @@ export class MigrationBuilder extends OperationRecorder {
310
308
  case 'alterColumn':
311
309
  return this.sqlGenerator.generateAlterColumnSql(operation.tableName, operation.columnName, operation.changes);
312
310
  case 'createIndex':
313
- return this.sqlGenerator.generateCreateIndex(operation.tableName, operation.index);
311
+ return this.sqlGenerator.generateCreateIndexFromDefinition(operation.tableName, operation.index);
314
312
  case 'dropIndex':
315
313
  return this.sqlGenerator.generateDropIndex(operation.tableName, operation.indexName);
316
314
  case 'addForeignKey':
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Fluent API for defining tables in migrations.
5
5
  */
6
- import { ddlText, normalizeIndexColumn } from '../../util/index.js';
6
+ import { indexNameParts, normalizeIndexColumn } from '../../util/index.js';
7
7
  import { derivedIndexName } from '../../util/sql.util.js';
8
8
  import { columnForeignKey, columnIndex } from '../generator/definitionToNode.js';
9
9
  import { ColumnBuilder } from './columnBuilder.js';
@@ -171,9 +171,7 @@ export class TableBuilder {
171
171
  const entries = columns.map(normalizeIndexColumn);
172
172
  this._indexes.push({
173
173
  ...rest,
174
- name: name ??
175
- derivedIndexName(this._name, entries.map((entry) => entry.column), unique),
176
- where: ddlText(rest.where, 'a partial-index predicate'),
174
+ name: name ?? derivedIndexName(this._name, indexNameParts(entries), unique),
177
175
  entries,
178
176
  unique,
179
177
  });
@@ -5,7 +5,7 @@
5
5
  * Enables type-safe migrations without raw SQL.
6
6
  */
7
7
  import type { ColumnNode, EnumValues, ForeignKeyAction } from '../../schema/types.js';
8
- import type { IndexColumnInput, IndexOptions, IndexSchema } from '../../type/index.js';
8
+ import type { EntityIndexColumn, Except, IndexColumnInput, IndexOptions, IndexSchema, QueryRaw } from '../../type/index.js';
9
9
  import type { ForeignKeySchema } from '../../type/migration.js';
10
10
  /**
11
11
  * Foreign key reference options.
@@ -91,6 +91,14 @@ export interface FullColumnDefinition extends ColumnDefinition {
91
91
  /** Index name (if indexed) */
92
92
  index?: string | boolean;
93
93
  }
94
+ /**
95
+ * An index as the builder records it, with the entries and options `@Index` takes: its SQL kept as `raw`
96
+ * until a generator renders it for the engine the migration runs on.
97
+ */
98
+ export type IndexDefinition = Except<IndexSchema, 'entries' | 'where'> & {
99
+ readonly entries: readonly EntityIndexColumn[];
100
+ readonly where?: QueryRaw;
101
+ };
94
102
  /**
95
103
  * Complete table definition.
96
104
  */
@@ -102,7 +110,7 @@ export interface TableDefinition {
102
110
  /** Primary key columns (for composite keys) */
103
111
  primaryKey?: string[];
104
112
  /** Index definitions */
105
- indexes: IndexSchema[];
113
+ indexes: IndexDefinition[];
106
114
  /** Foreign key definitions at table level */
107
115
  foreignKeys: ForeignKeySchema[];
108
116
  /** Table comment */
@@ -183,7 +191,7 @@ export interface RenameColumnOperation extends MigrationOperation {
183
191
  export interface CreateIndexOperation extends MigrationOperation {
184
192
  type: 'createIndex';
185
193
  tableName: string;
186
- index: IndexSchema;
194
+ index: IndexDefinition;
187
195
  ifNotExists?: boolean;
188
196
  }
189
197
  /**
@@ -89,7 +89,9 @@ export function indexNeedsRaw(index) {
89
89
  }
90
90
  function indexEntrySource(entry, propertyName, param) {
91
91
  if (entry.expression) {
92
- return rawTag(entry.column);
92
+ const expression = `() => ${rawTag(entry.column)}`;
93
+ const modifiers = significantModifiers(entry);
94
+ return modifiers.length === 0 ? expression : `{ column: ${expression}, ${modifiers.join(', ')} }`;
93
95
  }
94
96
  const column = memberSource(param, propertyName(entry.column));
95
97
  const modifiers = significantModifiers(entry);
@@ -1,12 +1,13 @@
1
1
  import type { ColumnNode, TableNode } from '../../schema/types.js';
2
2
  import type { ForeignKeySchema, IndexSchema } from '../../type/migration.js';
3
- import type { FullColumnDefinition, TableDefinition } from '../builder/types.js';
3
+ import type { QueryRaw } from '../../type/queryRaw.js';
4
+ import type { FullColumnDefinition, IndexDefinition, TableDefinition } from '../builder/types.js';
4
5
  /**
5
6
  * A migration builder's table definition as the AST nodes the generators render from, so a hand-written
6
- * `createTable` and an entity reach `generateCreateTableFromNode` in the same shape. Free functions and
7
- * not generator methods: nothing here consults the dialect.
7
+ * `createTable` and an entity reach `generateCreateTableFromNode` in the same shape, its SQL rendered by
8
+ * `render`. Free functions and not generator methods: the dialect reaches them only through `render`.
8
9
  */
9
- export declare function tableDefinitionToNode(def: TableDefinition): TableNode;
10
+ export declare function tableDefinitionToNode(def: TableDefinition, render: (sql: QueryRaw) => string): TableNode;
10
11
  /**
11
12
  * A builder's column as the AST node the generators render from.
12
13
  *
@@ -25,6 +26,8 @@ export declare function fullColumnDefinitionToNode(col: FullColumnDefinition, ta
25
26
  * Shared with `TableBuilder.build`, which lifts these into the table it is creating: written twice,
26
27
  * `addColumn` had no lift at all and silently emitted a column with no index.
27
28
  */
28
- export declare function columnIndex(tableName: string, col: FullColumnDefinition): IndexSchema | undefined;
29
+ export declare function columnIndex(tableName: string, col: FullColumnDefinition): Pick<IndexSchema, 'name' | 'entries' | 'unique'> | undefined;
30
+ /** An index the builder recorded, its SQL rendered by `render` into the text the schema holds. */
31
+ export declare function renderIndexDefinition(index: IndexDefinition, render: (sql: QueryRaw) => string): IndexSchema;
29
32
  /** The foreign key a column-level `references` declares, or nothing. Shared for the same reason. */
30
33
  export declare function columnForeignKey(col: FullColumnDefinition): ForeignKeySchema | undefined;
@@ -1,3 +1,4 @@
1
+ import { renderIndexColumn } from '../../util/ddlExpression.util.js';
1
2
  import { derivedForeignKeyName, derivedIndexName } from '../../util/sql.util.js';
2
3
  /**
3
4
  * A table the builder names but has not seen.
@@ -12,10 +13,10 @@ function unresolvedTable(name) {
12
13
  }
13
14
  /**
14
15
  * A migration builder's table definition as the AST nodes the generators render from, so a hand-written
15
- * `createTable` and an entity reach `generateCreateTableFromNode` in the same shape. Free functions and
16
- * not generator methods: nothing here consults the dialect.
16
+ * `createTable` and an entity reach `generateCreateTableFromNode` in the same shape, its SQL rendered by
17
+ * `render`. Free functions and not generator methods: the dialect reaches them only through `render`.
17
18
  */
18
- export function tableDefinitionToNode(def) {
19
+ export function tableDefinitionToNode(def, render) {
19
20
  const columns = new Map();
20
21
  const pkNodes = [];
21
22
  const table = {
@@ -40,7 +41,7 @@ export function tableDefinitionToNode(def) {
40
41
  : pkNodes;
41
42
  table.primaryKey = finalPrimaryKey;
42
43
  for (const idxDef of def.indexes) {
43
- table.indexes.push({ ...idxDef, table });
44
+ table.indexes.push({ ...renderIndexDefinition(idxDef, render), table });
44
45
  }
45
46
  for (const fkDef of def.foreignKeys) {
46
47
  const relNode = {
@@ -92,6 +93,14 @@ export function columnIndex(tableName, col) {
92
93
  unique: col.isUnique,
93
94
  };
94
95
  }
96
+ /** An index the builder recorded, its SQL rendered by `render` into the text the schema holds. */
97
+ export function renderIndexDefinition(index, render) {
98
+ return {
99
+ ...index,
100
+ entries: index.entries.map((entry) => renderIndexColumn(entry, render)),
101
+ where: index.where && render(index.where),
102
+ };
103
+ }
95
104
  /** The foreign key a column-level `references` declares, or nothing. Shared for the same reason. */
96
105
  export function columnForeignKey(col) {
97
106
  if (!col.foreignKey) {
@@ -8,6 +8,8 @@ export declare class MongoSchemaGenerator extends AbstractDialect implements Sch
8
8
  protected readonly featureDefaults: import("../../type/dialect.js").DialectFeatures;
9
9
  readonly insertIdSource: InsertIdSource;
10
10
  constructor(namingStrategy?: NamingStrategy, defaultForeignKeyAction?: ForeignKeyAction | undefined);
11
+ /** A collection has no SQL to render a check, a computed column or an index expression into. */
12
+ compileDdl(): string;
11
13
  /**
12
14
  * A document store has no cross-collection constraint, so unlike the SQL generator there is nothing to
13
15
  * defer and no order to respect: this is each collection and nothing more. `foreignKeys` is accepted
@@ -3,6 +3,7 @@ import { getMeta } from '../../entity/index.js';
3
3
  import { mongoDialectFeatures } from '../../mongo/mongoDialect.js';
4
4
  import { getKeys } from '../../util/index.js';
5
5
  import { derivedIndexName } from '../../util/sql.util.js';
6
+ import { renderIndexDefinition } from './definitionToNode.js';
6
7
  import { indexNodeToSchema } from './indexNodeToSchema.js';
7
8
  import { serializeMongoCommand } from './mongoCommand.js';
8
9
  export class MongoSchemaGenerator extends AbstractDialect {
@@ -14,6 +15,10 @@ export class MongoSchemaGenerator extends AbstractDialect {
14
15
  super({ namingStrategy });
15
16
  this.defaultForeignKeyAction = defaultForeignKeyAction;
16
17
  }
18
+ /** A collection has no SQL to render a check, a computed column or an index expression into. */
19
+ compileDdl() {
20
+ throw new TypeError('mongodb has no SQL to render a check, a computed column or an index expression into');
21
+ }
17
22
  /**
18
23
  * A document store has no cross-collection constraint, so unlike the SQL generator there is nothing to
19
24
  * defer and no order to respect: this is each collection and nothing more. `foreignKeys` is accepted
@@ -135,7 +140,7 @@ export class MongoSchemaGenerator extends AbstractDialect {
135
140
  generateCreateTableFromDefinition(table, _options) {
136
141
  return [
137
142
  serializeMongoCommand({ action: 'createCollection', name: table.name }),
138
- ...table.indexes.map((index) => this.generateCreateIndex(table.name, index)),
143
+ ...table.indexes.map((index) => this.generateCreateIndex(table.name, renderIndexDefinition(index, () => this.compileDdl()))),
139
144
  ];
140
145
  }
141
146
  generateRenameTableSql(oldName, newName) {
@@ -2,8 +2,8 @@ import type { AbstractSqlDialect } from '../dialect/index.js';
2
2
  import type { SchemaAST } from '../schema/schemaAST.js';
3
3
  import { type DiffOptions } from '../schema/schemaASTDiffer.js';
4
4
  import type { CanonicalType, ColumnNode, ForeignKeyAction, IndexNode, TableNode } from '../schema/types.js';
5
- import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, FieldMeta, FieldOptions, ForeignKeySchema, IndexSchema, MigratorDialect, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
6
- import type { FullColumnDefinition, TableDefinition } from './builder/types.js';
5
+ import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, EntityWhereMeta, FieldMeta, FieldOptions, ForeignKeySchema, IndexSchema, MigratorDialect, NamingStrategy, SchemaDiff, SchemaGenerator, SqlDdlGenerator, Type } from '../type/index.js';
6
+ import type { FullColumnDefinition, IndexDefinition, TableDefinition } from './builder/types.js';
7
7
  import { type IndexDdl, type TableDdl } from './ddl/index.js';
8
8
  /**
9
9
  * Unified SQL schema generator.
@@ -23,6 +23,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
23
23
  resolveTableAlias<E>(meta: EntityMeta<E>): string;
24
24
  resolveSchema<E>(meta: EntityMeta<E>): string | undefined;
25
25
  resolveColumnName(key: string, field: FieldOptions): string;
26
+ compileDdl(sql: EntityWhereMeta<object>, entity: Type<object>): string;
26
27
  /** Escape an identifier (table name, column name, etc.) */
27
28
  protected escapeId(identifier: string): string;
28
29
  /**
@@ -165,6 +166,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
165
166
  generateCreateTableFromDefinition(table: TableDefinition, options?: {
166
167
  ifNotExists?: boolean;
167
168
  }): string[];
169
+ generateCreateIndexFromDefinition(tableName: string, index: IndexDefinition): string;
168
170
  generateRenameTableSql(oldName: string, newName: string): string;
169
171
  /**
170
172
  * `ADD COLUMN`, plus the constraint and index the column declares.
@@ -217,7 +219,7 @@ export declare class SqlSchemaGenerator implements SqlDdlGenerator {
217
219
  * other way and the table is created under one name and compared under another, which reports every
218
220
  * table of a project using a naming strategy as both missing and unexpected.
219
221
  */
220
- export declare function buildEntityAST(generator: Pick<SchemaGenerator, 'resolveTableAlias' | 'resolveSchema' | 'resolveColumnName'>, entities: readonly Type<object>[], defaultForeignKeyAction?: ForeignKeyAction): SchemaAST;
222
+ export declare function buildEntityAST(generator: Pick<SchemaGenerator, 'resolveTableAlias' | 'resolveSchema' | 'resolveColumnName' | 'compileDdl'>, entities: readonly Type<object>[], defaultForeignKeyAction?: ForeignKeyAction): SchemaAST;
221
223
  /**
222
224
  * Synchronous factory for SQL schema generators only.
223
225
  * For MongoDB, use `createSchemaGeneratorAsync` from `./schemaGeneratorAsync.js` so the optional `mongodb` peer is not loaded at import time.
@@ -8,7 +8,7 @@ import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName } from '
8
8
  import { formatDefaultValue, SqlExpression } from './builder/expressions.js';
9
9
  import { indexDdlFor, tableDdlFor } from './ddl/index.js';
10
10
  import { sizedType } from './ddl/tableDdl.js';
11
- import { columnForeignKey, columnIndex, fullColumnDefinitionToNode, tableDefinitionToNode, } from './generator/definitionToNode.js';
11
+ import { columnForeignKey, columnIndex, fullColumnDefinitionToNode, renderIndexDefinition, tableDefinitionToNode, } from './generator/definitionToNode.js';
12
12
  import { indexNodeToSchema } from './generator/indexNodeToSchema.js';
13
13
  /**
14
14
  * Unified SQL schema generator.
@@ -45,6 +45,9 @@ export class SqlSchemaGenerator {
45
45
  resolveColumnName(key, field) {
46
46
  return this.dialect.resolveColumnName(key, field);
47
47
  }
48
+ compileDdl(sql, entity) {
49
+ return this.dialect.compileDdl(sql, entity);
50
+ }
48
51
  /** Escape an identifier (table name, column name, etc.) */
49
52
  escapeId(identifier) {
50
53
  return this.dialect.escapeId(identifier);
@@ -565,9 +568,12 @@ export class SqlSchemaGenerator {
565
568
  return this.generateCreateIndex(qualifyName(index.table.name, index.table.schema), indexNodeToSchema(index), options);
566
569
  }
567
570
  generateCreateTableFromDefinition(table, options = {}) {
568
- const tableNode = tableDefinitionToNode(table);
571
+ const tableNode = tableDefinitionToNode(table, (sql) => this.dialect.compileDdl(sql));
569
572
  return this.generateCreateTableFromNode(tableNode, options);
570
573
  }
574
+ generateCreateIndexFromDefinition(tableName, index) {
575
+ return this.generateCreateIndex(tableName, renderIndexDefinition(index, (sql) => this.dialect.compileDdl(sql)));
576
+ }
571
577
  generateRenameTableSql(oldName, newName) {
572
578
  return this.tableDdl.renameTable(oldName, newName);
573
579
  }
@@ -719,6 +725,7 @@ export function buildEntityAST(generator, entities, defaultForeignKeyAction) {
719
725
  resolveTableName: (meta) => generator.resolveTableAlias(meta),
720
726
  resolveSchema: (meta) => generator.resolveSchema(meta),
721
727
  resolveColumnName: (key, field) => generator.resolveColumnName(key, field),
728
+ compileDdl: (sql, entity) => generator.compileDdl(sql, entity),
722
729
  defaultForeignKeyAction,
723
730
  });
724
731
  }
@@ -263,7 +263,7 @@ export class MongoDialect extends AbstractDialect {
263
263
  * lock silently removes the mutual exclusion the caller asked for.
264
264
  */
265
265
  assertNoLock(q) {
266
- if (q.$lock !== undefined) {
266
+ if (q.$lock) {
267
267
  throw new TypeError('$lock (row-level locking) is not supported on MongoDB');
268
268
  }
269
269
  }
@@ -273,8 +273,7 @@ export class MsSqlDialect extends MergeSqlDialect {
273
273
  * scalar - which is why reading and writing need the two different binders here.
274
274
  */
275
275
  jsonScalarParam(ctx, value) {
276
- return (this.#jsonCompound(ctx, value) ??
277
- this.addValue(ctx.values, typeof value === 'boolean' ? JSON.stringify(value) : value));
276
+ return (this.#jsonCompound(ctx, value) ?? this.addValue(ctx, typeof value === 'boolean' ? JSON.stringify(value) : value));
278
277
  }
279
278
  /**
280
279
  * A value being *written* to a JSON path, which takes the type the driver sent: a `BIT` becomes a
@@ -286,7 +285,7 @@ export class MsSqlDialect extends MergeSqlDialect {
286
285
  if (compound) {
287
286
  return compound;
288
287
  }
289
- const placeholder = this.addValue(ctx.values, value);
288
+ const placeholder = this.addValue(ctx, value);
290
289
  return typeof value === 'boolean' ? `CAST(${placeholder} AS BIT)` : placeholder;
291
290
  }
292
291
  /**
@@ -300,8 +299,7 @@ export class MsSqlDialect extends MergeSqlDialect {
300
299
  if (value === null || typeof value !== 'object') {
301
300
  return undefined;
302
301
  }
303
- ctx.pushValue(JSON.stringify(value));
304
- return `JSON_QUERY(${this.placeholder(ctx.values.length)})`;
302
+ return `JSON_QUERY(${this.addValue(ctx, JSON.stringify(value))})`;
305
303
  }
306
304
  /** An exploded element compares as text here, so `$elemMatch` always expands per field. */
307
305
  jsonContainmentIsPartial = false;
@@ -6,7 +6,7 @@
6
6
  * - Database introspection results (TableSchema[])
7
7
  */
8
8
  import type { EntityGetter } from '../type/entity.js';
9
- import type { EntityMeta, FieldMeta, FieldOptions, Type } from '../type/index.js';
9
+ import type { EntityMeta, EntityWhereMeta, FieldMeta, FieldOptions, Type } from '../type/index.js';
10
10
  import type { NamingStrategy } from '../type/namingStrategy.js';
11
11
  import { SchemaAST } from './schemaAST.js';
12
12
  import { type CanonicalType, type ForeignKeyAction } from './types.js';
@@ -24,6 +24,11 @@ export interface BuildSchemaASTOptions {
24
24
  namingStrategy?: NamingStrategy;
25
25
  /** Default action for foreign key ON DELETE and ON UPDATE clauses */
26
26
  defaultForeignKeyAction?: ForeignKeyAction;
27
+ /**
28
+ * The text of SQL an entity declares - a check, a stored computed column, an index expression or
29
+ * predicate - which only a dialect can render. `buildEntityAST` supplies it from the generator.
30
+ */
31
+ compileDdl?: (sql: EntityWhereMeta<object>, entity: Type<object>) => string;
27
32
  }
28
33
  /**
29
34
  * Build a SchemaAST from entity classes (decorated with `@Entity`, `@Field`, etc.).
@@ -6,7 +6,7 @@
6
6
  * - Database introspection results (TableSchema[])
7
7
  */
8
8
  import { getMeta, soleIdOf } from '../entity/metadata/definition.js';
9
- import { ddlText } from '../util/ddlExpression.util.js';
9
+ import { indexNameParts, renderIndexColumn } from '../util/ddlExpression.util.js';
10
10
  import { isInlinedExpression } from '../util/field.util.js';
11
11
  import { isSoleIdField } from '../util/field.util.js';
12
12
  import { isAutoIncrement } from '../util/field.util.js';
@@ -30,6 +30,7 @@ export function buildSchemaAST(entities, options = {}) {
30
30
  resolveSchema: options.resolveSchema ?? ((m) => m.schema),
31
31
  resolveColumnName: options.resolveColumnName ?? ((k, f) => namingStrategy?.columnName(f.name ?? k) ?? f.name ?? k),
32
32
  defaultForeignKeyAction: options.defaultForeignKeyAction ?? DEFAULT_FOREIGN_KEY_ACTION,
33
+ compileDdl: options.compileDdl ?? refuseDdl,
33
34
  };
34
35
  for (const pass of [addTableFromEntity, addRelationshipsFromEntity, addIndexesFromEntity]) {
35
36
  for (const entity of entities) {
@@ -38,6 +39,10 @@ export function buildSchemaAST(entities, options = {}) {
38
39
  }
39
40
  return ctx.ast;
40
41
  }
42
+ /** The `compileDdl` of a build given no dialect, which has nothing to render an entity's SQL with. */
43
+ function refuseDdl() {
44
+ throw new TypeError('building the schema of an entity that declares SQL (a check, a stored computed column, an index expression or predicate) needs a dialect to render it: pass `compileDdl`, as `buildEntityAST` does');
45
+ }
41
46
  /**
42
47
  * Resolve the canonical type for a field, inheriting from the referenced
43
48
  * entity's primary key when the field is a foreign-key reference
@@ -78,7 +83,7 @@ function addTableFromEntity(ctx, meta) {
78
83
  const tableName = ctx.resolveTableName(meta);
79
84
  const table = createTableNode(tableName, ctx.resolveSchema(meta));
80
85
  const { columns, primaryKey } = table;
81
- table.checks?.push(...(meta.checks ?? []));
86
+ table.checks?.push(...(meta.checks ?? []).map(({ name, where }) => ({ name, expression: ctx.compileDdl(where, meta.entity) })));
82
87
  // Add columns from fields
83
88
  for (const [key, field] of definedEntries(meta.fields)) {
84
89
  // An inlined expression has no column; a stored one is a column like any other.
@@ -98,7 +103,7 @@ function addTableFromEntity(ctx, meta) {
98
103
  isPrimaryKey,
99
104
  isAutoIncrement: isAutoIncrement(field, isSoleKey),
100
105
  isUnique: field.unique ?? false,
101
- generatedAs: ddlText(field.computed, `the computed column '${columnName}'`),
106
+ generatedAs: field.computed && ctx.compileDdl(field.computed, meta.entity),
102
107
  comment: field.comment,
103
108
  enum: field.enum,
104
109
  table,
@@ -237,27 +242,23 @@ function resolveIncludeColumn(ctx, meta, column) {
237
242
  function addCompositeIndex(ctx, table, meta, idxMeta) {
238
243
  // An entry survives if it is an expression (nothing to resolve) or names a column that exists;
239
244
  // an index left with none is dropped, the same as one naming only unknown columns always was.
240
- const entries = idxMeta.columns
241
- .map((entry) => {
242
- if (entry.expression)
243
- return entry;
245
+ const resolved = idxMeta.columns.flatMap((entry) => {
246
+ if (typeof entry.column !== 'string')
247
+ return [entry];
244
248
  const field = meta.fields[entry.column];
245
249
  const column = field && ctx.resolveColumnName(entry.column, field);
246
- return column && table.columns.has(column) ? { ...entry, column } : undefined;
247
- })
248
- .filter((entry) => entry !== undefined);
249
- if (!entries.length)
250
+ return column && table.columns.has(column) ? [{ ...entry, column }] : [];
251
+ });
252
+ if (!resolved.length)
250
253
  return;
251
- // An index over expressions alone has no column names to build a default name from.
252
- const named = entries.map((entry, at) => (entry.expression ? `expr${at}` : entry.column));
253
254
  ctx.ast.addIndex({
254
- name: idxMeta.name ?? derivedIndexName(table.name, named),
255
+ name: idxMeta.name ?? derivedIndexName(table.name, indexNameParts(resolved)),
255
256
  table,
256
- entries,
257
+ entries: resolved.map((entry) => renderIndexColumn(entry, (sql) => ctx.compileDdl(sql, meta.entity))),
257
258
  include: idxMeta.include?.map((column) => resolveIncludeColumn(ctx, meta, column)),
258
259
  unique: idxMeta.unique ?? false,
259
260
  type: idxMeta.type,
260
- where: idxMeta.where,
261
+ where: idxMeta.where && ctx.compileDdl(idxMeta.where, meta.entity),
261
262
  distance: idxMeta.distance,
262
263
  m: idxMeta.m,
263
264
  efConstruction: idxMeta.efConstruction,