@mikro-orm/sql 7.2.7-dev.5 → 7.2.7-dev.7

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.
@@ -18,6 +18,8 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
18
18
  evaluateFormula(formula: (...args: any[]) => string | Raw, columns: any, table: FormulaTable): string;
19
19
  /** For TPT entities, returns ownProps (columns in this table); otherwise returns all props. */
20
20
  protected getTableProps<T extends object>(meta: EntityMetadata<T>): EntityProperty<T>[];
21
+ /** Columns reloaded by INSERT: generated/default columns any row leaves to the database, and explicit `returning` hints. */
22
+ protected getInsertReturningProperties<T extends object>(meta: EntityMetadata<T>, data: EntityDictionary<T>[]): EntityProperty<T>[];
21
23
  /** Columns reloaded by UPDATE: raw values, PKs, generated/version columns and explicit `returning` hints. */
22
24
  protected getUpdateReturningProperties<T extends object>(meta: EntityMetadata<T>, data: EntityDictionary<T>[]): EntityProperty<T>[];
23
25
  /** Column expression for the `returning` clause of batch updates, applying `convertToJSValueSQL` of custom types. */
@@ -63,6 +65,8 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
63
65
  private buildCloneFields;
64
66
  private getCloneableProps;
65
67
  nativeInsertMany<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>, transform?: (sql: string) => string): Promise<QueryResult<T>>;
68
+ /** Resolves the primary key of every inserted row from the supplied data or the returned rows, and reports them via `insertedIds` when all are known. */
69
+ protected resolveInsertedPrimaryKeys<T extends object>(meta: EntityMetadata<T>, data: EntityDictionary<T>[], res: QueryResult<T>): any[][];
66
70
  nativeUpdate<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T> & UpsertOptions<T>): Promise<QueryResult<T>>;
67
71
  nativeUpdateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>[], data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T> & UpsertManyOptions<T>, transform?: (sql: string, params: any[]) => string): Promise<QueryResult<T>>;
68
72
  nativeDelete<T extends object>(entityName: EntityName<T>, where: FilterQuery<T> | string | any, options?: DeleteOptions<T>): Promise<QueryResult<T>>;
@@ -49,6 +49,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
49
49
  getTableProps(meta) {
50
50
  return meta.inheritanceType === 'tpt' && meta.ownProps ? meta.ownProps : meta.props;
51
51
  }
52
+ /** Columns reloaded by INSERT: generated/default columns any row leaves to the database, and explicit `returning` hints. */
53
+ getInsertReturningProperties(meta, data) {
54
+ return this.getTableProps(meta).filter(prop => prop.returning ||
55
+ (((prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated) &&
56
+ data.some(d => !(prop.name in d) || isRaw(d[prop.name]))));
57
+ }
52
58
  /** Columns reloaded by UPDATE: raw values, PKs, generated/version columns and explicit `returning` hints. */
53
59
  getUpdateReturningProperties(meta, data) {
54
60
  const returning = new Set();
@@ -801,10 +807,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
801
807
  ? '(' + fields.map(k => this.platform.quoteIdentifier(k)).join(', ') + ')'
802
808
  : `(${this.platform.quoteIdentifier(pks[0])})`;
803
809
  if (this.platform.usesOutputStatement()) {
804
- const returningProps = this.getTableProps(meta).filter(prop => prop.returning ||
805
- (((prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated) &&
806
- (!(prop.name in data[0]) || isRaw(data[0][prop.name]))));
807
- const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
810
+ const returningFields = Utils.flatten(this.getInsertReturningProperties(meta, data).map(prop => prop.fieldNames));
808
811
  sql +=
809
812
  returningFields.length > 0
810
813
  ? ` output ${returningFields.map(field => 'inserted.' + this.platform.quoteIdentifier(field)).join(', ')}`
@@ -931,10 +934,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
931
934
  .join(', ');
932
935
  }
933
936
  if (meta && this.platform.usesReturningStatement()) {
934
- const returningProps = this.getTableProps(meta).filter(prop => prop.returning ||
935
- (((prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated) &&
936
- (!(prop.name in data[0]) || isRaw(data[0][prop.name]))));
937
- const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
937
+ const returningFields = Utils.flatten(this.getInsertReturningProperties(meta, data).map(prop => prop.fieldNames));
938
938
  /* v8 ignore next */
939
939
  sql +=
940
940
  returningFields.length > 0
@@ -945,22 +945,35 @@ export class AbstractSqlDriver extends DatabaseDriver {
945
945
  sql = transform(sql);
946
946
  }
947
947
  const res = await this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options));
948
- let pk;
949
- if (pks.length > 1) {
950
- // owner has composite pk
951
- pk = data.map(d => Utils.getOrderedPrimaryKeys(d, meta));
952
- }
953
- else {
954
- res.row ??= {};
955
- res.rows ??= [];
956
- pk = data.map((d, i) => d[pks[0]] ?? res.rows[i]?.[pks[0]]).map(d => [d]);
957
- res.insertId = res.insertId || res.row[pks[0]];
958
- }
948
+ const pk = this.resolveInsertedPrimaryKeys(meta, data, res);
959
949
  for (let i = 0; i < collections.length; i++) {
960
950
  await this.processManyToMany(meta, pk[i], collections[i], false, options);
961
951
  }
962
952
  return res;
963
953
  }
954
+ /** Resolves the primary key of every inserted row from the supplied data or the returned rows, and reports them via `insertedIds` when all are known. */
955
+ resolveInsertedPrimaryKeys(meta, data, res) {
956
+ const pks = this.getPrimaryKeyFields(meta);
957
+ if (pks.length > 1) {
958
+ // owner has composite pk
959
+ const pk = data.map(d => Utils.getOrderedPrimaryKeys(d, meta));
960
+ res.insertedIds = pk;
961
+ return pk;
962
+ }
963
+ res.row ??= {};
964
+ res.rows ??= [];
965
+ const ids = data.map((d, i) => {
966
+ // a raw pk expression is resolved by the database, its value only exists in the returned row
967
+ const supplied = d[meta.primaryKeys[0]] ?? d[pks[0]];
968
+ return supplied == null || isRaw(supplied) ? res.rows[i]?.[pks[0]] : supplied;
969
+ });
970
+ res.insertId = res.insertId || res.row[pks[0]];
971
+ // drivers without `returning` leave the ids unknown here, keep the `insertId` fallback for them
972
+ if (ids.every(id => id != null)) {
973
+ res.insertedIds = ids;
974
+ }
975
+ return ids.map(id => [id]);
976
+ }
964
977
  async nativeUpdate(entityName, where, data, options = {}) {
965
978
  options.convertCustomTypes ??= true;
966
979
  const meta = this.metadata.get(entityName);
@@ -2544,7 +2557,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
2544
2557
  if (ret.length > 0 && !hasExplicitFields && addFormulas) {
2545
2558
  // `toString()` must yield the quoted alias like `QueryBuilder.finalize()`, which dedupes formulas by rendered SQL
2546
2559
  const quotedAlias = this.platform.quoteIdentifier(alias);
2547
- const columns = meta.createColumnMappingObject(alias, quotedAlias.toString());
2560
+ // TPT parent aliases are only known once the parent joins exist
2561
+ qb.ensureTPTJoins();
2562
+ const columns = meta.createColumnMappingObject(p => qb.helper.getTPTAliasForProperty(p.name, alias), quotedAlias.toString());
2548
2563
  const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
2549
2564
  for (const prop of meta.props) {
2550
2565
  if (lazyProps.includes(prop)) {
@@ -59,6 +59,7 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
59
59
  };
60
60
  isBigIntProperty(prop: EntityProperty): boolean;
61
61
  getArrayDeclarationSQL(): string;
62
+ supportsArrayOverlap(): boolean;
62
63
  getFloatDeclarationSQL(): string;
63
64
  getDoubleDeclarationSQL(): string;
64
65
  getEnumTypeDeclarationSQL(column: {
@@ -199,6 +199,9 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
199
199
  getArrayDeclarationSQL() {
200
200
  return 'text[]';
201
201
  }
202
+ supportsArrayOverlap() {
203
+ return true;
204
+ }
202
205
  getFloatDeclarationSQL() {
203
206
  return 'real';
204
207
  }
@@ -25,6 +25,7 @@ export class PostgreSqlExceptionConverter extends ExceptionConverter {
25
25
  break;
26
26
  case '23502':
27
27
  return new NotNullConstraintViolationException(exception);
28
+ case '23001':
28
29
  case '23503':
29
30
  return new ForeignKeyConstraintViolationException(exception);
30
31
  case '23505':
@@ -409,14 +409,19 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
409
409
  const key = this.getTableKey(col);
410
410
  ret[key] ??= [];
411
411
  let type = col.data_type.toLowerCase() === 'array' ? col.udt_name.replace(/^_(.*)$/, '$1[]') : col.udt_name;
412
+ // `udt_name` drops array element typmods (`_numeric` for `numeric(12,2)[]`), recover them from `format_type`
413
+ const arrayModifier = type.endsWith('[]')
414
+ ? /^[^(]*(\([^)]*\))[^(]*\[\]$/.exec(col.format_type ?? '')?.[1]
415
+ : undefined;
416
+ if (arrayModifier) {
417
+ type = `${type.slice(0, -2)}${arrayModifier}[]`;
418
+ }
412
419
  if (col.data_type === 'USER-DEFINED' &&
413
420
  col.udt_schema &&
414
421
  col.udt_schema !== this.platform.getDefaultSchemaName()) {
415
422
  type = `${col.udt_schema}.${type}`;
416
423
  }
417
- if (type === 'bpchar') {
418
- type = 'char';
419
- }
424
+ type = type.replace(/^bpchar\b/, 'char');
420
425
  // postgis stores the geometry/geography typmod (`geometry(point,4326)`) in atttypmod, which
421
426
  // information_schema does not expose — recover the full type via format_type so it round-trips
422
427
  if ((col.udt_name === 'geometry' || col.udt_name === 'geography') && col.format_type) {
@@ -242,7 +242,8 @@ export class SqliteSchemaHelper extends SchemaHelper {
242
242
  }
243
243
  for (let i = cols.length - 1; i >= 0; i--) {
244
244
  const col = cols[i];
245
- const re = ` *, *[\`"']?${col.name}[\`"']? (.*)`;
245
+ // the first column is not preceded by a comma
246
+ const re = ` *(?:^|,) *[\`"']?${col.name}[\`"']? (.*)`;
246
247
  const columnDef = new RegExp(re, 'i').exec(columnsDef);
247
248
  if (columnDef) {
248
249
  columns[col.name] = { name: col.name, definition: columnDef[1] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.2.7-dev.5",
3
+ "version": "7.2.7-dev.7",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
@@ -53,7 +53,7 @@
53
53
  "@mikro-orm/core": "^7.2.6"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.2.7-dev.5"
56
+ "@mikro-orm/core": "7.2.7-dev.7"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -27,6 +27,7 @@ export declare class CriteriaNode<T extends object> implements ICriteriaNode<T>
27
27
  parentPath?: string;
28
28
  }): string;
29
29
  private isPivotJoin;
30
+ protected isOperatorPayload(payload: any): boolean;
30
31
  getPivotPath(path: string): string;
31
32
  aliased(field: string, alias?: string): string;
32
33
  isStrict(): boolean;
@@ -125,8 +125,17 @@ export class CriteriaNode {
125
125
  this.payload instanceof RegExp ||
126
126
  this.payload instanceof Date ||
127
127
  rawField;
128
- const operator = Utils.isObject(this.payload) && Utils.getObjectQueryKeys(this.payload).every(k => Utils.isOperator(k, false));
129
- return this.prop.kind === ReferenceKind.MANY_TO_MANY && (scalar || operator);
128
+ return this.prop.kind === ReferenceKind.MANY_TO_MANY && (scalar || this.isOperatorPayload(this.payload));
129
+ }
130
+ isOperatorPayload(payload) {
131
+ return (Utils.isObject(payload) &&
132
+ Utils.getObjectQueryKeys(payload).every(k => {
133
+ if (k === '$not' && payload[k] instanceof CriteriaNode && Utils.isPlainObject(payload[k].payload)) {
134
+ // `$not` wrapping entity conditions is a condition on the target entity, not on the relation itself
135
+ return Utils.getObjectQueryKeys(payload[k].payload).every(ik => Utils.isOperator(ik, false));
136
+ }
137
+ return Utils.isOperator(k, false);
138
+ }));
130
139
  }
131
140
  getPivotPath(path) {
132
141
  return `${path}[pivot]`;
@@ -283,18 +283,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
283
283
  const embeddable = this.prop.kind === ReferenceKind.EMBEDDED;
284
284
  const knownKey = [ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE, ReferenceKind.EMBEDDED].includes(this.prop.kind) ||
285
285
  (this.prop.kind === ReferenceKind.ONE_TO_ONE && this.prop.owner);
286
- const operatorKeys = knownKey &&
287
- keys.every(key => {
288
- if (key === '$not') {
289
- // $not wraps conditions like $and/$or, check if it wraps entity property conditions (needs auto-join)
290
- // vs simple operator conditions on the FK (doesn't need auto-join)
291
- const childPayload = this.payload[key].payload;
292
- if (Utils.isPlainObject(childPayload)) {
293
- return Utils.getObjectQueryKeys(childPayload).every(k => Utils.isOperator(k, false));
294
- }
295
- }
296
- return Utils.isOperator(key, false);
297
- });
286
+ const operatorKeys = knownKey && this.isOperatorPayload(this.payload);
298
287
  const primaryKeys = knownKey &&
299
288
  keys.every(key => {
300
289
  if (key === '$not') {
@@ -322,8 +311,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
322
311
  this.payload instanceof RegExp ||
323
312
  this.payload instanceof Date ||
324
313
  rawField;
325
- const operator = Utils.isPlainObject(this.payload) &&
326
- Utils.getObjectQueryKeys(this.payload).every(k => Utils.isOperator(k, false));
314
+ const operator = Utils.isPlainObject(this.payload) && this.isOperatorPayload(this.payload);
327
315
  const field = `${alias}.${this.prop.name}`;
328
316
  const method = qb.hasFlag(QueryFlag.INFER_POPULATE) ? 'joinAndSelect' : 'join';
329
317
  const path = this.getPath();
@@ -1003,6 +1003,10 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
1003
1003
  processPopulateHint(): void;
1004
1004
  private processPopulateWhere;
1005
1005
  private mergeOnConditions;
1006
+ /**
1007
+ * `$not` can be moved to a join's `on` clause as a whole only when all of its conditions target that join.
1008
+ */
1009
+ private getNotConditionAlias;
1006
1010
  /**
1007
1011
  * `$or` branches can be moved to a join's `on` clause only when they all target that same join
1008
1012
  * and stay flat — a partial `$or` in the `on` clause would drop rows matching a sibling branch,
@@ -1012,9 +1016,9 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
1012
1016
  private canDistributeOrBranches;
1013
1017
  private getOrBranchAliases;
1014
1018
  /**
1015
- * An entity filter's `$or` that cannot be distributed is applied intact to the `on` clause of the
1016
- * outermost join common to all targeted joins, nesting the targeted joins under it so the clause
1017
- * can reference their aliases.
1019
+ * A condition that cannot be distributed (an entity filter's `$or`, or a `$not` spanning several joins)
1020
+ * is applied intact to the `on` clause of the outermost join common to all targeted joins, nesting
1021
+ * the targeted joins under it so the clause can reference their aliases.
1018
1022
  */
1019
1023
  private mergeFilterOrCondition;
1020
1024
  /**
@@ -1558,7 +1558,10 @@ export class QueryBuilder {
1558
1558
  field = asMatch[1].trim();
1559
1559
  customAlias = asMatch[2];
1560
1560
  }
1561
- const join = Object.keys(this.#state.joins).find(k => field === k.substring(0, k.indexOf('#')));
1561
+ // an aliased field is always a column, never a relation to expand, even when an FK column is named like its relation
1562
+ const join = customAlias
1563
+ ? undefined
1564
+ : Object.keys(this.#state.joins).find(k => field === k.substring(0, k.indexOf('#')));
1562
1565
  if (join && type === 'where') {
1563
1566
  ret.push(...this.helper.mapJoinColumns(this.type, this.#state.joins[join]));
1564
1567
  return;
@@ -2102,7 +2105,13 @@ export class QueryBuilder {
2102
2105
  }
2103
2106
  mergeOnConditions(joins, cond, filter, op) {
2104
2107
  for (const k of Object.keys(cond)) {
2105
- if (Utils.isOperator(k)) {
2108
+ const notAlias = k === '$not' ? this.getNotConditionAlias(cond[k], joins) : undefined;
2109
+ if (k === '$not' && !notAlias) {
2110
+ // splitting a `$not` across joins would drop the negation, so it is kept intact on their common ancestor join
2111
+ this.mergeFilterOrCondition([{ $not: cond[k] }], joins);
2112
+ continue;
2113
+ }
2114
+ if (Utils.isOperator(k) && !notAlias) {
2106
2115
  if (Array.isArray(cond[k])) {
2107
2116
  if (k === '$or' && !this.canDistributeOrBranches(cond[k], joins)) {
2108
2117
  // entity filters have no other sink, so their `$or` is kept intact on the outermost targeted join instead
@@ -2116,7 +2125,7 @@ export class QueryBuilder {
2116
2125
  /* v8 ignore next */
2117
2126
  this.mergeOnConditions(joins, cond[k], filter, k);
2118
2127
  }
2119
- const [alias] = this.helper.splitField(k);
2128
+ const alias = notAlias ?? this.helper.splitField(k)[0];
2120
2129
  const join = joins.find(j => j.alias === alias);
2121
2130
  if (join) {
2122
2131
  const parentJoin = joins.find(j => j.alias === join.ownerAlias);
@@ -2145,6 +2154,14 @@ export class QueryBuilder {
2145
2154
  }
2146
2155
  }
2147
2156
  }
2157
+ /**
2158
+ * `$not` can be moved to a join's `on` clause as a whole only when all of its conditions target that join.
2159
+ */
2160
+ getNotConditionAlias(cond, joins) {
2161
+ const aliases = this.getOrBranchAliases([cond]);
2162
+ const [alias] = aliases;
2163
+ return aliases.size === 1 && joins.some(j => j.alias === alias) ? alias : undefined;
2164
+ }
2148
2165
  /**
2149
2166
  * `$or` branches can be moved to a join's `on` clause only when they all target that same join
2150
2167
  * and stay flat — a partial `$or` in the `on` clause would drop rows matching a sibling branch,
@@ -2173,9 +2190,9 @@ export class QueryBuilder {
2173
2190
  return aliases;
2174
2191
  }
2175
2192
  /**
2176
- * An entity filter's `$or` that cannot be distributed is applied intact to the `on` clause of the
2177
- * outermost join common to all targeted joins, nesting the targeted joins under it so the clause
2178
- * can reference their aliases.
2193
+ * A condition that cannot be distributed (an entity filter's `$or`, or a `$not` spanning several joins)
2194
+ * is applied intact to the `on` clause of the outermost join common to all targeted joins, nesting
2195
+ * the targeted joins under it so the clause can reference their aliases.
2179
2196
  */
2180
2197
  mergeFilterOrCondition(branches, joins) {
2181
2198
  const aliases = this.getOrBranchAliases(branches);
@@ -623,6 +623,19 @@ export class QueryBuilderHelper {
623
623
  parts.push(`${this.#platform.quoteIdentifier(mappedKey)} ${replacement} ${sql}`);
624
624
  params.push(...query.params);
625
625
  }
626
+ else if (['$in', '$nin'].includes(op) &&
627
+ prop?.customType instanceof ArrayType &&
628
+ Array.isArray(opValue) &&
629
+ !opValue.every((v) => Array.isArray(v))) {
630
+ // a flat list on an array column matches any of its elements, like in mongo
631
+ if (!this.#platform.supportsArrayOverlap()) {
632
+ throw new ValidationError(`${op} on array property ${prop.name} expects a list of arrays, use $eq for an exact match`);
633
+ }
634
+ const mappedKey = this.mapper(key, type, opValue, null);
635
+ const sql = `${this.#platform.quoteIdentifier(mappedKey)} && ?`;
636
+ parts.push(op === '$in' ? sql : `not (${sql})`);
637
+ params.push(prop.customType.convertToDatabaseValue(opValue, this.#platform, { fromQuery: true, key: op, mode: 'query' }));
638
+ }
626
639
  else {
627
640
  const mappedKey = this.mapper(key, type, opValue, null);
628
641
  const val = this.getValueReplacement(fields, opValue, params, op, prop, rowValues);
@@ -641,16 +654,15 @@ export class QueryBuilderHelper {
641
654
  }
642
655
  return `(${tmp.join(', ')})`;
643
656
  }
644
- // array custom types (e.g. `$contains`/`$overlap` on `string[]` columns) are converted to a single
645
- // array literal, so we emit a single bound parameter — not one per element wrapped in a tuple
657
+ // array custom types convert a whole array into one literal, so each candidate array binds a single parameter
646
658
  if (prop?.customType instanceof ArrayType) {
647
- const item = prop.customType.convertToDatabaseValue(value, this.#platform, {
648
- fromQuery: true,
649
- key,
650
- mode: 'query',
651
- });
652
- params.push(item);
653
- return '?';
659
+ const list = key === '$in' || key === '$nin';
660
+ const items = list ? value : [value];
661
+ for (const item of items) {
662
+ params.push(prop.customType.convertToDatabaseValue(item, this.#platform, { fromQuery: true, key, mode: 'query' }));
663
+ }
664
+ const sql = items.map(() => '?').join(', ');
665
+ return list ? `(${sql})` : sql;
654
666
  }
655
667
  value.forEach(p => params.push(p));
656
668
  return `(${value.map(() => '?').join(', ')})`;
@@ -760,6 +760,10 @@ export class DatabaseTable {
760
760
  !idx.primary &&
761
761
  !idx.where &&
762
762
  !this.hasAdvancedIndexOptions(idx));
763
+ // `getMappedType('timestamptz(3)[]')` resolves to the element type so diffing keeps the modifier, but the property is still an array
764
+ if (column.type?.endsWith('[]') && !column.enumItems?.length) {
765
+ column = { ...column, mappedType: Type.getType(t.array) };
766
+ }
763
767
  const kind = this.getReferenceKind(fk, unique);
764
768
  const runtimeType = this.getPropertyTypeForColumn(namingStrategy, column, fk);
765
769
  const type = fk