uql-orm 0.55.0 → 0.57.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 (109) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/uql-browser.min.js +2 -2
  3. package/dist/browser/uql-browser.min.js.map +4 -4
  4. package/dist/bunSql/bunSql.util.js +1 -1
  5. package/dist/cockroachdb/cockroachDialect.d.ts +5 -2
  6. package/dist/cockroachdb/cockroachDialect.js +2 -10
  7. package/dist/d1/d1SqliteDialect.d.ts +1 -0
  8. package/dist/d1/d1SqliteDialect.js +2 -0
  9. package/dist/dialect/abstractSqlDialect.d.ts +195 -32
  10. package/dist/dialect/abstractSqlDialect.js +406 -199
  11. package/dist/dialect/aliases.d.ts +10 -7
  12. package/dist/dialect/aliases.js +12 -7
  13. package/dist/dialect/hydrateColumn.d.ts +8 -2
  14. package/dist/dialect/hydrateColumn.js +33 -1
  15. package/dist/dialect/jsonSql.d.ts +13 -5
  16. package/dist/dialect/jsonSql.js +24 -7
  17. package/dist/dialect/mysqlLikeSqlDialect.d.ts +30 -2
  18. package/dist/dialect/mysqlLikeSqlDialect.js +58 -7
  19. package/dist/dialect/pgLikeSqlDialect.d.ts +20 -20
  20. package/dist/dialect/pgLikeSqlDialect.js +25 -50
  21. package/dist/dialect/pgVectorMetrics.d.ts +13 -0
  22. package/dist/dialect/pgVectorMetrics.js +17 -0
  23. package/dist/dialect/queryContext.d.ts +3 -7
  24. package/dist/dialect/queryContext.js +13 -8
  25. package/dist/dialect/queryJoins.d.ts +8 -4
  26. package/dist/dialect/queryJoins.js +27 -15
  27. package/dist/dialect/vectorSqlDialect.d.ts +2 -2
  28. package/dist/dialect/vectorSqlDialect.js +2 -3
  29. package/dist/entity/index.d.ts +1 -1
  30. package/dist/entity/index.js +1 -1
  31. package/dist/entity/metadata/definition.d.ts +4 -2
  32. package/dist/entity/metadata/definition.js +27 -29
  33. package/dist/maria/mariaDialect.d.ts +13 -6
  34. package/dist/maria/mariaDialect.js +29 -9
  35. package/dist/migrate/builder/splitSqlStatements.js +2 -2
  36. package/dist/migrate/cli.d.ts +2 -3
  37. package/dist/migrate/cli.js +4 -11
  38. package/dist/migrate/codegen/fieldOptionsSource.js +1 -1
  39. package/dist/migrate/ddl/index.d.ts +1 -5
  40. package/dist/migrate/ddl/index.js +14 -25
  41. package/dist/migrate/ddl/indexDdl.d.ts +11 -2
  42. package/dist/migrate/ddl/indexDdl.js +17 -1
  43. package/dist/migrate/ddl/mssqlIndexDdl.d.ts +10 -0
  44. package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
  45. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +10 -17
  46. package/dist/migrate/ddl/mysqlIndexDdl.js +16 -27
  47. package/dist/migrate/ddl/pgIndexDdl.d.ts +18 -3
  48. package/dist/migrate/ddl/pgIndexDdl.js +29 -3
  49. package/dist/migrate/drift/driftDetector.js +21 -8
  50. package/dist/migrate/generator/mongoSchemaGenerator.js +1 -1
  51. package/dist/migrate/introspection/baseSqlIntrospector.d.ts +1 -1
  52. package/dist/migrate/introspection/baseSqlIntrospector.js +65 -76
  53. package/dist/migrate/introspection/mssqlIntrospector.js +2 -1
  54. package/dist/migrate/introspection/mysqlIntrospector.js +5 -8
  55. package/dist/migrate/introspection/sqliteIntrospector.js +2 -5
  56. package/dist/migrate/migrator.d.ts +7 -4
  57. package/dist/migrate/migrator.js +9 -14
  58. package/dist/migrate/schemaGenerator.d.ts +3 -3
  59. package/dist/migrate/schemaGenerator.js +7 -13
  60. package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
  61. package/dist/mongo/mongoDialect.d.ts +31 -18
  62. package/dist/mongo/mongoDialect.js +147 -108
  63. package/dist/mongo/mongodbQuerier.d.ts +10 -17
  64. package/dist/mongo/mongodbQuerier.js +34 -108
  65. package/dist/mssql/mssqlDialect.d.ts +16 -0
  66. package/dist/mssql/mssqlDialect.js +26 -4
  67. package/dist/mysql/mysqlDialect.d.ts +2 -0
  68. package/dist/mysql/mysqlDialect.js +4 -0
  69. package/dist/querier/abstractQuerier.d.ts +20 -36
  70. package/dist/querier/abstractQuerier.js +44 -143
  71. package/dist/querier/abstractQuerierPool.d.ts +2 -2
  72. package/dist/querier/abstractSqlQuerier.d.ts +11 -22
  73. package/dist/querier/abstractSqlQuerier.js +49 -53
  74. package/dist/schema/canonicalType.js +4 -6
  75. package/dist/schema/dependencyGraph.js +2 -4
  76. package/dist/schema/indexDifferences.js +5 -5
  77. package/dist/schema/schemaASTBuilder.js +34 -12
  78. package/dist/schema/schemaASTDiffer.d.ts +10 -2
  79. package/dist/schema/schemaASTDiffer.js +17 -16
  80. package/dist/schema/types.d.ts +1 -1
  81. package/dist/sqlite/sqliteDialect.d.ts +20 -1
  82. package/dist/sqlite/sqliteDialect.js +40 -8
  83. package/dist/turso/tursoDialect.d.ts +2 -0
  84. package/dist/turso/tursoDialect.js +2 -0
  85. package/dist/type/config.d.ts +2 -2
  86. package/dist/type/dialect.d.ts +4 -5
  87. package/dist/type/entity.d.ts +2 -1
  88. package/dist/type/migratorDialect.d.ts +4 -0
  89. package/dist/type/querier.d.ts +6 -6
  90. package/dist/type/query.d.ts +25 -48
  91. package/dist/type/query.js +10 -5
  92. package/dist/type/queryAggregate.d.ts +10 -10
  93. package/dist/type/queryAggregate.js +1 -1
  94. package/dist/type/universalQuerier.d.ts +4 -4
  95. package/dist/util/dialect.util.d.ts +8 -2
  96. package/dist/util/dialect.util.js +19 -0
  97. package/dist/util/field.util.d.ts +5 -0
  98. package/dist/util/field.util.js +19 -0
  99. package/dist/util/logger.d.ts +10 -1
  100. package/dist/util/logger.js +18 -0
  101. package/dist/util/object.util.d.ts +4 -0
  102. package/dist/util/object.util.js +8 -0
  103. package/dist/util/relationQuery.util.d.ts +15 -68
  104. package/dist/util/relationQuery.util.js +35 -83
  105. package/dist/util/rowKey.util.d.ts +1 -11
  106. package/dist/util/rowKey.util.js +1 -13
  107. package/package.json +1 -1
  108. package/dist/querier/relationCount.d.ts +0 -16
  109. package/dist/querier/relationCount.js +0 -121
@@ -10,14 +10,14 @@ export class SqlQueryContext {
10
10
  statement;
11
11
  sqlChunks = [];
12
12
  params;
13
- aliasCounter = 0;
13
+ tableAliases = new Set();
14
14
  /**
15
15
  * @param dialect The SQL dialect used to determine how values should be formatted as placeholders.
16
16
  * @param params An existing values array to bind into instead of a fresh one - shared by a
17
17
  * fragment context built via {@link AbstractSqlDialect.buildFragment}, so a bound value's
18
18
  * placeholder is numbered correctly against the real query from the moment it's added, rather
19
19
  * than needing to be reconciled after the fact.
20
- * @param statement The context this one renders a fragment of, which owns the alias counter: a
20
+ * @param statement The context this one renders a fragment of, which owns the claimed aliases: a
21
21
  * fragment is part of one statement, so its aliases have to be unique across the whole of it.
22
22
  */
23
23
  constructor(dialect, params = [], statement) {
@@ -62,12 +62,17 @@ export class SqlQueryContext {
62
62
  this.params.push(...values.map((v) => this.dialect.normalizeValue(v)));
63
63
  return this;
64
64
  }
65
- /**
66
- * A fresh alias unique within the statement being built, e.g. `nextAlias('_uql_elem')` ->
67
- * `'_uql_elem_1'`, `'_uql_elem_2'`, ...
68
- */
69
- nextAlias(prefix) {
70
- return this.statement ? this.statement.nextAlias(prefix) : `${prefix}_${++this.aliasCounter}`;
65
+ claimAlias(name, parent) {
66
+ if (this.statement) {
67
+ return this.statement.claimAlias(name, parent);
68
+ }
69
+ const reserved = parent?.toLowerCase();
70
+ let alias = name;
71
+ for (let n = 2; this.tableAliases.has(alias.toLowerCase()) || alias.toLowerCase() === reserved; n++) {
72
+ alias = `${name}_${n}`;
73
+ }
74
+ this.tableAliases.add(alias.toLowerCase());
75
+ return alias;
71
76
  }
72
77
  /**
73
78
  * Returns the complete SQL query string by joining all accumulated chunks.
@@ -1,5 +1,4 @@
1
- import type { EntityMeta, Query, QuerySortMap, RelationMeta, Type } from '../type/index.js';
2
- import { type RelationQuery } from '../util/index.js';
1
+ import type { EntityMeta, Query, QuerySortMap, RelationMeta, RelationQuery, Type } from '../type/index.js';
3
2
  /**
4
3
  * One relation a statement joins, keyed by the alias its columns are addressed by (`tax`,
5
4
  * `tax.category`). `projected` tells a `$populate` join, whose columns are selected, from one only
@@ -8,8 +7,10 @@ import { type RelationQuery } from '../util/index.js';
8
7
  export type QueryJoin = {
9
8
  /** The relation key on its parent, which is how MongoDB names the field a `$lookup` adds. */
10
9
  readonly key: string;
11
- /** Dotted path from the queried entity, which is how the SQL dialects alias the join. */
10
+ /** Dotted path from the queried entity, which is what a joined row's columns answer under. */
12
11
  readonly path: string;
12
+ /** The alias the statement reads it through: its path, unless another table of the statement took it. */
13
+ readonly alias: string;
13
14
  readonly entity: Type<object>;
14
15
  readonly meta: EntityMeta<object>;
15
16
  readonly relation: RelationMeta;
@@ -38,14 +39,17 @@ export type QuerySortOptions = {
38
39
  * related column needs that relation joined just as much as selecting it does. The two sources meet
39
40
  * here, so the columns, the `ORDER BY` and the row lock cannot disagree about what is in the
40
41
  * statement. `$sort` contributes to-one relations only; the rest is rejected where it is rendered.
42
+ * `claimAlias` names each join's table, parents first.
41
43
  */
42
- export declare function resolveQueryJoins<E>(meta: EntityMeta<E>, q: Query<E>): QueryJoins;
44
+ export declare function resolveQueryJoins<E>(meta: EntityMeta<E>, q: Query<E>, claimAlias?: (path: string) => string): QueryJoins;
43
45
  /**
44
46
  * Whether a join drops parents that have no match, which is the one thing a join does to *how many*
45
47
  * rows a read returns rather than how wide they are. A count that skips the joins has to be told, or
46
48
  * it counts the parents the read will never hand back.
47
49
  */
48
50
  export declare function hasRequiredJoin<E>(meta: EntityMeta<E>, q: Query<E>): boolean;
51
+ /** Whether a statement aggregates a relation's rows: a to-many off its own row, or off a row it joins. */
52
+ export declare function aggregatesRelations<E>(meta: EntityMeta<E>, q: Query<E>): boolean;
49
53
  /**
50
54
  * The join an ordering may address at `path`, with the relation's own sort map, or why it may not.
51
55
  * Every backend answers this the same way - a to-many has no single value to order by, a relation
@@ -1,19 +1,20 @@
1
- import { getMeta } from '../entity/index.js';
2
- import { getKeys, getRelationRequestSummary, isToManyRelation, parseRelationAtKey, } from '../util/index.js';
1
+ import { getMeta, relationOf } from '../entity/index.js';
2
+ import { getKeys, getRelationRequestSummary, isToManyRelation, parseRelationAtKey } from '../util/index.js';
3
3
  export const NO_JOINS = new Map();
4
4
  /**
5
5
  * What the statement joins, from the whole query rather than from `$populate` alone: ordering by a
6
6
  * related column needs that relation joined just as much as selecting it does. The two sources meet
7
7
  * here, so the columns, the `ORDER BY` and the row lock cannot disagree about what is in the
8
8
  * statement. `$sort` contributes to-one relations only; the rest is rejected where it is rendered.
9
+ * `claimAlias` names each join's table, parents first.
9
10
  */
10
- export function resolveQueryJoins(meta, q) {
11
+ export function resolveQueryJoins(meta, q, claimAlias = (path) => path) {
11
12
  if (!q.$populate && !q.$sort) {
12
13
  return NO_JOINS;
13
14
  }
14
15
  const joins = new Map();
15
- addPopulateJoins(joins, meta, q.$populate);
16
- addSortJoins(joins, meta, q.$sort);
16
+ addPopulateJoins(joins, claimAlias, meta, q.$populate);
17
+ addSortJoins(joins, claimAlias, meta, q.$sort);
17
18
  return joins;
18
19
  }
19
20
  /**
@@ -29,7 +30,19 @@ export function hasRequiredJoin(meta, q) {
29
30
  }
30
31
  return false;
31
32
  }
32
- function addJoin(joins, parent, key, relation, query, required, projected) {
33
+ /** Whether a statement aggregates a relation's rows: a to-many off its own row, or off a row it joins. */
34
+ export function aggregatesRelations(meta, q) {
35
+ if (getRelationRequestSummary(meta, q.$populate).toManyKeys.length) {
36
+ return true;
37
+ }
38
+ for (const join of resolveQueryJoins(meta, q).values()) {
39
+ if (getRelationRequestSummary(join.meta, join.query.$populate).toManyKeys.length) {
40
+ return true;
41
+ }
42
+ }
43
+ return false;
44
+ }
45
+ function addJoin(joins, claimAlias, parent, key, relation, query, required, projected) {
33
46
  const path = parent ? `${parent.path}.${key}` : key;
34
47
  const existing = joins.get(path);
35
48
  // `$populate` runs first, so an already-joined relation keeps its columns and its `$required`
@@ -41,6 +54,7 @@ function addJoin(joins, parent, key, relation, query, required, projected) {
41
54
  const join = {
42
55
  key,
43
56
  path,
57
+ alias: claimAlias(path),
44
58
  entity,
45
59
  meta: getMeta(entity),
46
60
  relation,
@@ -52,17 +66,15 @@ function addJoin(joins, parent, key, relation, query, required, projected) {
52
66
  joins.set(path, join);
53
67
  return join;
54
68
  }
55
- function addPopulateJoins(joins, meta, populate, parent) {
69
+ function addPopulateJoins(joins, claimAlias, meta, populate, parent) {
56
70
  for (const key of getRelationRequestSummary(meta, populate).joinableKeys) {
57
- const relation = meta.relations[key];
58
- if (!relation)
59
- continue;
71
+ const relation = relationOf(meta, key);
60
72
  const { query, required } = parseRelationAtKey(key, populate);
61
- const join = addJoin(joins, parent, key, relation, query, required, true);
62
- addPopulateJoins(joins, join.meta, query.$populate, join);
73
+ const join = addJoin(joins, claimAlias, parent, key, relation, query, required, true);
74
+ addPopulateJoins(joins, claimAlias, join.meta, query.$populate, join);
63
75
  }
64
76
  }
65
- function addSortJoins(joins, meta, sort, parent) {
77
+ function addSortJoins(joins, claimAlias, meta, sort, parent) {
66
78
  if (!sort) {
67
79
  return;
68
80
  }
@@ -74,8 +86,8 @@ function addSortJoins(joins, meta, sort, parent) {
74
86
  if (!relation || isToManyRelation(relation) || !isSortMap(value)) {
75
87
  continue;
76
88
  }
77
- const join = addJoin(joins, parent, key, relation, {}, false, false);
78
- addSortJoins(joins, join.meta, value, join);
89
+ const join = addJoin(joins, claimAlias, parent, key, relation, {}, false, false);
90
+ addSortJoins(joins, claimAlias, join.meta, value, join);
79
91
  }
80
92
  }
81
93
  /**
@@ -78,8 +78,8 @@ export declare abstract class VectorSqlDialect extends AbstractDialect {
78
78
  */
79
79
  supportedVectorType(cast: VectorCast): VectorCast;
80
80
  /**
81
- * Append a vector distance projection.
82
- * Delegates to `appendVectorSort` so each dialect's distance syntax is written once.
81
+ * The distance a vector `$sort` projects, which the projection names after `$project`. Delegates to
82
+ * `appendVectorSort` so each dialect's distance syntax is written once.
83
83
  */
84
84
  protected appendVectorProjection<E>(ctx: QueryContext, meta: EntityMeta<E>, key: string, search: QueryVectorSearch): void;
85
85
  /**
@@ -96,8 +96,8 @@ export class VectorSqlDialect extends AbstractDialect {
96
96
  return this.hasNarrowVectorTypes ? cast : 'vector';
97
97
  }
98
98
  /**
99
- * Append a vector distance projection.
100
- * Delegates to `appendVectorSort` so each dialect's distance syntax is written once.
99
+ * The distance a vector `$sort` projects, which the projection names after `$project`. Delegates to
100
+ * `appendVectorSort` so each dialect's distance syntax is written once.
101
101
  */
102
102
  appendVectorProjection(ctx, meta, key, search) {
103
103
  const alias = search.$project;
@@ -108,7 +108,6 @@ export class VectorSqlDialect extends AbstractDialect {
108
108
  throw new TypeError(`$project '${alias}' collides with a field of '${entityName(meta)}'`);
109
109
  }
110
110
  this.appendVectorSort(ctx, meta, key, search);
111
- ctx.append(` AS ${this.escapeId(alias)}`);
112
111
  }
113
112
  /**
114
113
  * The distance expression, in whichever of the two shapes this dialect spells it. One method for
@@ -1,3 +1,3 @@
1
1
  export * from './decorator/entity.js';
2
2
  export * from './decorator/members.js';
3
- export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, removeEntity, assertSoleId, fieldOf, idOf, namesKey, soleIdOf, } from './metadata/definition.js';
3
+ export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, removeEntity, assertSoleId, fieldOf, idOf, namesKey, relationOf, soleIdOf, } from './metadata/definition.js';
@@ -1,3 +1,3 @@
1
1
  export * from './decorator/entity.js';
2
2
  export * from './decorator/members.js';
3
- export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, removeEntity, assertSoleId, fieldOf, idOf, namesKey, soleIdOf, } from './metadata/definition.js';
3
+ export { defineEntity, defineField, defineFilter, defineHook, defineId, defineIndex, defineRelation, getEntities, getMeta, removeEntity, assertSoleId, fieldOf, idOf, namesKey, relationOf, soleIdOf, } from './metadata/definition.js';
@@ -1,4 +1,4 @@
1
- import type { EntityData, EntityIndexInput, EntityMembers, EntityMeta, EntityOptions, FieldKey, FieldMeta, FieldOptions, FilterOptions, HookEvent, IdKey, RelationOptions, Type, WrittenId } from '../../type/index.js';
1
+ import type { EntityData, EntityIndexInput, EntityMembers, EntityMeta, EntityOptions, FieldKey, FieldMeta, FieldOptions, FilterOptions, HookEvent, IdKey, RelationKey, RelationMeta, RelationOptions, Type, WrittenId } from '../../type/index.js';
2
2
  export declare function defineField<E>(entity: Type<E>, key: string, opts?: FieldOptions): EntityMeta<E>;
3
3
  export declare function defineId<E>(entity: Type<E>, key: string, opts: FieldOptions): EntityMeta<E>;
4
4
  export declare function defineRelation<E>(entity: Type<E>, key: string, opts: RelationOptions): EntityMeta<E>;
@@ -31,7 +31,9 @@ export declare function assertSoleId<E>(meta: EntityMeta<E>, what: string): void
31
31
  /** The entity's one primary key, for a path that cannot express a composite. See {@link assertSoleId}. */
32
32
  export declare function soleIdOf<E>(meta: EntityMeta<E>, what: string): IdKey<E>;
33
33
  /** The field `key` names, for a caller that took `key` from the metadata itself. */
34
- export declare function fieldOf<E>(meta: EntityMeta<E>, key: string): FieldMeta;
34
+ export declare function fieldOf<E>(meta: EntityMeta<E>, key: FieldKey<E>): FieldMeta;
35
+ /** The relation `key` names, for a caller that took `key` from the metadata itself. */
36
+ export declare function relationOf<E>(meta: EntityMeta<E>, key: RelationKey<E>): RelationMeta;
35
37
  /**
36
38
  * Whether the caller named every column of the row's primary key, so {@link idOf} can name the row.
37
39
  *
@@ -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, } from '../../util/index.js';
3
+ import { entityName, fieldOptionConflict, getKeys, ddlText, 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
@@ -95,9 +95,7 @@ export function defineFilter(entity, name, opts) {
95
95
  * API converge on one registration path before anything is finalized.
96
96
  */
97
97
  export function applyMembers(entity, specs) {
98
- for (const [key, spec] of Object.entries(specs?.fields ?? {})) {
99
- if (!spec)
100
- continue;
98
+ for (const [key, spec] of definedEntries(specs?.fields ?? {})) {
101
99
  if (spec.isId) {
102
100
  defineId(entity, key, spec);
103
101
  }
@@ -105,12 +103,11 @@ export function applyMembers(entity, specs) {
105
103
  defineField(entity, key, spec);
106
104
  }
107
105
  }
108
- for (const [key, spec] of Object.entries(specs?.relations ?? {})) {
109
- if (spec)
110
- defineRelation(entity, key, spec);
106
+ for (const [key, spec] of definedEntries(specs?.relations ?? {})) {
107
+ defineRelation(entity, key, spec);
111
108
  }
112
- for (const [event, methodNames] of Object.entries(specs?.hooks ?? {})) {
113
- for (const methodName of methodNames ?? []) {
109
+ for (const [event, methodNames] of definedEntries(specs?.hooks ?? {})) {
110
+ for (const methodName of methodNames) {
114
111
  defineHook(entity, methodName, event);
115
112
  }
116
113
  }
@@ -143,9 +140,8 @@ export function defineEntity(entity, opts = {}) {
143
140
  for (const index of opts.indexes ?? []) {
144
141
  defineIndex(entity, index);
145
142
  }
146
- for (const [name, filter] of Object.entries(opts.filters ?? {})) {
147
- if (filter)
148
- defineFilter(entity, name, filter);
143
+ for (const [name, filter] of definedEntries(opts.filters ?? {})) {
144
+ defineFilter(entity, name, filter);
149
145
  }
150
146
  if (!hasKeys(meta.fields)) {
151
147
  throw TypeError(`'${entity.name}' must have fields`);
@@ -227,6 +223,14 @@ export function fieldOf(meta, key) {
227
223
  }
228
224
  return field;
229
225
  }
226
+ /** The relation `key` names, for a caller that took `key` from the metadata itself. */
227
+ export function relationOf(meta, key) {
228
+ const relation = meta.relations[key];
229
+ if (!relation) {
230
+ throw new TypeError(`'${meta.entity.name}' has no relation '${key}'`);
231
+ }
232
+ return relation;
233
+ }
230
234
  /**
231
235
  * Whether the caller named every column of the row's primary key, so {@link idOf} can name the row.
232
236
  *
@@ -303,11 +307,9 @@ export function getMeta(entity) {
303
307
  return fillRelations(meta);
304
308
  }
305
309
  function fillRelations(meta) {
306
- for (const relKey in meta.relations) {
310
+ for (const [relKey, relation] of definedEntries(meta.relations)) {
307
311
  // The authored view: `mappedBy` may still be the callback and `references` unset until this settles them.
308
- const relOpts = meta.relations[relKey];
309
- if (!relOpts)
310
- continue;
312
+ const relOpts = relation;
311
313
  const at = `'${meta.entity.name}.${relKey}'`;
312
314
  if (relOpts.mappedBy) {
313
315
  fillInverseSide(at, meta, relOpts);
@@ -335,9 +337,9 @@ function fillRelations(meta) {
335
337
  function fillOwningSide(at, meta, relKey, relOpts) {
336
338
  const relMeta = ensureMeta(relOpts.entity());
337
339
  if (relOpts.through) {
338
- // Both columns live on the junction, whatever the cardinality: `fillToManyThroughRelation`,
339
- // `deleteRelations` and every dialect read them as junction columns. A composite key contributes
340
- // one pair per column of it, which is what makes the join address a whole key rather than part.
340
+ // Both columns live on the junction, whatever the cardinality: `deleteRelations` and every dialect
341
+ // read them as junction columns. A composite key contributes one pair per column of it, which is
342
+ // what makes the join address a whole key rather than part.
341
343
  relOpts.references = [
342
344
  ...meta.ids.map((key) => ({ local: junctionColumn(meta, key), foreign: key })),
343
345
  ...relMeta.ids.map((key) => ({ local: junctionColumn(relMeta, key), foreign: key })),
@@ -363,7 +365,7 @@ function fillOwningSide(at, meta, relKey, relOpts) {
363
365
  for (const { local, foreign } of relOpts.references) {
364
366
  fields[local] ??= {
365
367
  name: local,
366
- type: relMeta.fields[foreign]?.type ?? Number,
368
+ type: fieldOf(relMeta, foreign).type ?? Number,
367
369
  references: relOpts.entity,
368
370
  referencedKey: foreign,
369
371
  typeFromReference: true,
@@ -413,9 +415,8 @@ function fillInverseSide(at, meta, relOpts) {
413
415
  * own cardinality and `cascade`.
414
416
  */
415
417
  function fillForeignKeyRelations(meta) {
416
- const joined = new Set(getKeys(meta.relations).flatMap((key) => meta.relations[key]?.references.map(({ local }) => local) ?? []));
417
- for (const fieldKey of getKeys(meta.fields)) {
418
- const references = meta.fields[fieldKey]?.references;
418
+ const joined = new Set(definedEntries(meta.relations).flatMap(([, relation]) => relation.references.map(({ local }) => local)));
419
+ for (const [fieldKey, { references }] of definedEntries(meta.fields)) {
419
420
  if (!references || joined.has(fieldKey))
420
421
  continue;
421
422
  const target = ensureMeta(references());
@@ -445,7 +446,7 @@ function fillForeignKeyRelations(meta) {
445
446
  }
446
447
  /** `<entityName><IdColumn>`, not the `<relationKey>Id` an owning to-one derives: a junction row has no relation key to borrow from. */
447
448
  function junctionColumn(meta, idKey) {
448
- return lowerFirst(entityName(meta)) + upperFirst(meta.fields[idKey]?.name ?? idKey);
449
+ return lowerFirst(entityName(meta)) + upperFirst(fieldOf(meta, idKey).name ?? idKey);
449
450
  }
450
451
  /** A callback only reads one property off the key map, and that property is the key, so one serves every entity. */
451
452
  const RELATION_KEY_MAP = new Proxy({}, { get: (_, key) => key });
@@ -478,11 +479,8 @@ function extendMeta(target, source) {
478
479
  if (source.hooks) {
479
480
  if (!target.hooks)
480
481
  target.hooks = {};
481
- for (const event of Object.keys(source.hooks)) {
482
- const sourceList = source.hooks[event];
483
- if (sourceList?.length) {
484
- target.hooks[event] = [...sourceList, ...(target.hooks[event] ?? [])];
485
- }
482
+ for (const [event, sourceList] of definedEntries(source.hooks)) {
483
+ target.hooks[event] = [...sourceList, ...(target.hooks[event] ?? [])];
486
484
  }
487
485
  }
488
486
  }
@@ -1,5 +1,6 @@
1
+ import { type RelationRows } from '../dialect/abstractSqlDialect.js';
1
2
  import { MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
2
- import type { DialectFeatures, EntityMeta, FieldOptions, Query, QueryContext, QueryOptions, Type, VectorDistance, VectorMetric } from '../type/index.js';
3
+ import type { DialectFeatures, EntityMeta, FieldOptions, Query, QueryContext, Type, VectorDistance, VectorMetric } from '../type/index.js';
3
4
  export declare class MariaDialect extends MysqlLikeSqlDialect {
4
5
  readonly dialectName = "mariadb";
5
6
  readonly insertIdSource = "returning";
@@ -10,6 +11,11 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
10
11
  * and `CREATE INDEX` takes `IF NOT EXISTS` - which MySQL's grammar has no place for.
11
12
  */
12
13
  protected readonly featureOverrides: Partial<DialectFeatures>;
14
+ /**
15
+ * A derived table here reads no column of the statement around it, so the aggregate reads the
16
+ * related table itself, and orders and pages inside `JSON_ARRAYAGG`, which takes both.
17
+ */
18
+ protected appendRelationArray(ctx: QueryContext, { entity, query, alias, joins }: RelationRows): void;
13
19
  protected upsertReturning<E>(meta: EntityMeta<E>): string;
14
20
  /**
15
21
  * MariaDB supports neither MySQL's `->`/`->>` shorthand nor the base's chained form. `JSON_VALUE`
@@ -36,12 +42,13 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
36
42
  */
37
43
  protected appendVectorValue(ctx: QueryContext, value: readonly unknown[]): void;
38
44
  /**
39
- * `SET STATEMENT mhnsw_ef_search=N FOR SELECT ...` - MariaDB scopes a variable to one statement, so
40
- * the tuning needs neither a transaction nor a restore afterwards, and cannot leak to the next
41
- * query on this pooled connection. That is why it prefixes the SQL here instead of coming back
42
- * from `vectorTuningStatements`, which is Postgres's `SET LOCAL` shape.
45
+ * `mhnsw_ef_search` too, where a vector search is tuned. A setting scoped to one statement needs
46
+ * neither a transaction nor a restore, and cannot leak to the next query on this pooled connection,
47
+ * which is why the tuning is not `vectorTuningStatements`, Postgres's `SET LOCAL` shape.
43
48
  */
44
- find<E>(ctx: QueryContext, entity: Type<E>, q?: Query<E>, opts?: QueryOptions, totalAlias?: string): void;
49
+ protected statementSettings<E>(entity: Type<E>, q: Query<E>): string[];
50
+ /** `SET STATEMENT ... FOR`, which scopes a variable to the statement it prefixes. */
51
+ protected applySettings(sql: string, settings: readonly string[]): string;
45
52
  /** The reverse: selecting a `VECTOR` column raw yields that blob, so it is read back as text. */
46
53
  protected selectFieldExpr(escapedColumn: string, field: FieldOptions): string;
47
54
  }
@@ -1,3 +1,4 @@
1
+ import { relationTermKey } from '../dialect/abstractSqlDialect.js';
1
2
  import { jsonPath } from '../dialect/jsonSql.js';
2
3
  import { MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
3
4
  import { getMeta } from '../entity/index.js';
@@ -18,6 +19,24 @@ export class MariaDialect extends MysqlLikeSqlDialect {
18
19
  vectorIndexRequiresNotNull: true,
19
20
  indexIfNotExists: true,
20
21
  };
22
+ /**
23
+ * A derived table here reads no column of the statement around it, so the aggregate reads the
24
+ * related table itself, and orders and pages inside `JSON_ARRAYAGG`, which takes both.
25
+ */
26
+ appendRelationArray(ctx, { entity, query, alias, joins }) {
27
+ const meta = getMeta(entity);
28
+ const terms = this.projection(ctx, entity, query, { prefix: alias, json: true }, joins);
29
+ const sortOpts = { prefix: alias, joins, distinct: query.$distinct };
30
+ const order = this.buildFragment(ctx, (fragmentCtx) => this.sort(fragmentCtx, entity, query.$sort, sortOpts));
31
+ const page = this.buildFragment(ctx, (fragmentCtx) => this.pager(fragmentCtx, query));
32
+ const from = this.buildFragment(ctx, (fragmentCtx) => {
33
+ this.selectRelationJoins(fragmentCtx, meta, alias, joins);
34
+ this.where(fragmentCtx, entity, query.$where, { prefix: alias });
35
+ });
36
+ const object = this.jsonObject(terms.map((term) => [relationTermKey(term), term.sql]));
37
+ const rows = `${query.$distinct ? 'DISTINCT ' : ''}${object}${order}${page}`;
38
+ ctx.append(`COALESCE((SELECT JSON_ARRAYAGG(${rows}) FROM ${this.tableRef(meta, alias).ref}${from}), JSON_ARRAY())`);
39
+ }
21
40
  upsertReturning(meta) {
22
41
  const returning = this.returningId(meta);
23
42
  return returning ? ` ${returning}` : '';
@@ -61,18 +80,19 @@ export class MariaDialect extends MysqlLikeSqlDialect {
61
80
  ctx.append(')');
62
81
  }
63
82
  /**
64
- * `SET STATEMENT mhnsw_ef_search=N FOR SELECT ...` - MariaDB scopes a variable to one statement, so
65
- * the tuning needs neither a transaction nor a restore afterwards, and cannot leak to the next
66
- * query on this pooled connection. That is why it prefixes the SQL here instead of coming back
67
- * from `vectorTuningStatements`, which is Postgres's `SET LOCAL` shape.
83
+ * `mhnsw_ef_search` too, where a vector search is tuned. A setting scoped to one statement needs
84
+ * neither a transaction nor a restore, and cannot leak to the next query on this pooled connection,
85
+ * which is why the tuning is not `vectorTuningStatements`, Postgres's `SET LOCAL` shape.
68
86
  */
69
- find(ctx, entity, q = {}, opts, totalAlias) {
87
+ statementSettings(entity, q) {
70
88
  // `$candidates` first: `getMeta` would otherwise be resolved on every read, to discover that
71
89
  // almost none of them tune anything.
72
- if (q.$candidates !== undefined && this.tunedVectorIndex(getMeta(entity), q)) {
73
- ctx.append(`SET STATEMENT mhnsw_ef_search=${q.$candidates} FOR `);
74
- }
75
- super.find(ctx, entity, q, opts, totalAlias);
90
+ const tuned = q.$candidates !== undefined && this.tunedVectorIndex(getMeta(entity), q);
91
+ return [...(tuned ? [`mhnsw_ef_search=${q.$candidates}`] : []), ...super.statementSettings(entity, q)];
92
+ }
93
+ /** `SET STATEMENT ... FOR`, which scopes a variable to the statement it prefixes. */
94
+ applySettings(sql, settings) {
95
+ return `SET STATEMENT ${settings.join(', ')} FOR ${sql}`;
76
96
  }
77
97
  /** The reverse: selecting a `VECTOR` column raw yields that blob, so it is read back as text. */
78
98
  selectFieldExpr(escapedColumn, field) {
@@ -26,11 +26,11 @@ export function splitSqlStatements(sql) {
26
26
  const masterRegex = /'(?:''|\\['\\]|[^'])*(?:'|(?=$))|"(?:""|\\["\\]|[^"])*(?:"|(?=$))|`(?:``|\\[`\\]|[^`])*(?:`|(?=$))|\$(?<tag>[a-zA-Z0-9_]*)\$[\s\S]*?(?:\$\k<tag>|(?=$))|--.*|\/\*[\s\S]*?(?:\*\/|(?=$))|;/g;
27
27
  for (const match of sql.matchAll(masterRegex)) {
28
28
  if (match[0] === ';') {
29
- const stmt = sql.substring(lastIndex, match.index ?? 0).trim();
29
+ const stmt = sql.substring(lastIndex, match.index).trim();
30
30
  if (stmt) {
31
31
  statements.push(stmt);
32
32
  }
33
- lastIndex = (match.index ?? 0) + match[0].length;
33
+ lastIndex = match.index + match[0].length;
34
34
  }
35
35
  }
36
36
  const lastStmt = sql.substring(lastIndex).trim();
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import type { AbstractDialect } from '../dialect/index.js';
3
2
  import type { ForeignKeyAction } from '../schema/types.js';
4
- import type { Config } from '../type/index.js';
3
+ import type { Config, MigratorDialect } from '../type/index.js';
5
4
  import { Migrator } from './migrator.js';
6
5
  import { createSchemaGeneratorAsync } from './schemaGeneratorAsync.js';
7
6
  /** Sync helper for SQL dialects only; returns `undefined` for MongoDB - use {@link createSchemaGeneratorAsync}. */
8
- export declare function getSchemaGenerator(dialect: AbstractDialect, defaultForeignKeyAction?: ForeignKeyAction): import("./schemaGenerator.js").SqlSchemaGenerator | undefined;
7
+ export declare function getSchemaGenerator(dialect: MigratorDialect, defaultForeignKeyAction?: ForeignKeyAction): import("./schemaGenerator.js").SqlSchemaGenerator | undefined;
9
8
  export { createSchemaGeneratorAsync };
10
9
  export declare function main(args?: string[]): Promise<void>;
11
10
  export declare function runUp(migrator: Migrator, args: string[]): Promise<void>;
@@ -34,7 +34,6 @@ export async function main(args = process.argv.slice(2)) {
34
34
  try {
35
35
  const config = await loadConfig(customPath);
36
36
  assertCliConfig(config);
37
- const dialectName = config.pool.dialect.dialectName ?? 'postgres';
38
37
  const options = {
39
38
  migrationsPath: config.migrationsPath ?? './migrations',
40
39
  tableName: config.tableName,
@@ -43,13 +42,7 @@ export async function main(args = process.argv.slice(2)) {
43
42
  defaultForeignKeyAction: config.defaultForeignKeyAction,
44
43
  };
45
44
  const migrator = new Migrator(config.pool, options);
46
- if (!migrator.schemaGenerator) {
47
- const generator = await createSchemaGeneratorAsync(config.pool.dialect, config.defaultForeignKeyAction);
48
- if (!generator) {
49
- throw new TypeError(`Could not find a schema generator for dialect: ${dialectName}`);
50
- }
51
- migrator.setSchemaGenerator(generator);
52
- }
45
+ await migrator.ensureSchemaGenerator();
53
46
  switch (command) {
54
47
  case 'up':
55
48
  await runUp(migrator, filteredArgs.slice(1));
@@ -314,8 +307,8 @@ function printDriftGroup(title, drifts, icon, showSuggestion) {
314
307
  if (drift.expected && drift.actual) {
315
308
  console.log(` Expected: ${drift.expected}, Actual: ${drift.actual}`);
316
309
  }
317
- if (showSuggestion && drift.suggestion) {
318
- console.log(` → ${drift.suggestion}`);
310
+ if (showSuggestion) {
311
+ console.log(` -> ${drift.suggestion}`);
319
312
  }
320
313
  }
321
314
  console.log('');
@@ -365,7 +358,7 @@ Configuration:
365
358
  Create a uql.config.ts or uql.config.js file in your project root.
366
359
  You can also specify a custom config path using --config or -c.
367
360
  The CLI requires pool.dialect (dialect id = pool.dialect.dialectName).
368
- See the repo README section "Driver → pool → dialect class".
361
+ See the repo README section "Driver -> pool -> dialect class".
369
362
 
370
363
  export default {
371
364
  pool: new PgQuerierPool({ ... }),
@@ -16,7 +16,7 @@ const OPTION_SOURCE = {
16
16
  type: (col) => {
17
17
  const columnType = canonicalToColumnType(col.type);
18
18
  return [
19
- ...(columnType ? [`columnType: ${quoted(columnType)}`] : []),
19
+ `columnType: ${quoted(columnType)}`,
20
20
  ...(col.type.length && col.type.category === 'string' ? [`length: ${col.type.length}`] : []),
21
21
  ...(col.type.precision === undefined ? [] : [`precision: ${col.type.precision}`]),
22
22
  ...(col.type.precision !== undefined && col.type.scale !== undefined ? [`scale: ${col.type.scale}`] : []),
@@ -2,15 +2,11 @@ import type { AbstractSqlDialect } from '../../dialect/abstractSqlDialect.js';
2
2
  import { IndexDdl } from './indexDdl.js';
3
3
  import { TableDdl } from './tableDdl.js';
4
4
  export { IndexDdl } from './indexDdl.js';
5
+ export { MsSqlIndexDdl } from './mssqlIndexDdl.js';
5
6
  export { MsSqlTableDdl } from './mssqlTableDdl.js';
6
7
  export { MariaIndexDdl, MySqlIndexDdl, MysqlLikeIndexDdl } from './mysqlIndexDdl.js';
7
8
  export { CockroachIndexDdl, PgIndexDdl } from './pgIndexDdl.js';
8
9
  export { TableDdl } from './tableDdl.js';
9
- /**
10
- * The index DDL a dialect gets, most specific first. `instanceof` rather than the `dialectName`
11
- * {@link tableDdlFor} reads, because each family's index DDL is typed to its dialect and the narrowing
12
- * is what hands it one. Anything else gets the portable form, which is SQLite's.
13
- */
14
10
  export declare function indexDdlFor(dialect: AbstractSqlDialect): IndexDdl;
15
11
  /**
16
12
  * The table DDL a dialect gets: SQL Server's, or the portable form every other engine takes. By
@@ -1,40 +1,29 @@
1
- import { CockroachDialect } from '../../cockroachdb/cockroachDialect.js';
2
- import { MysqlLikeSqlDialect } from '../../dialect/mysqlLikeSqlDialect.js';
3
- import { PgLikeSqlDialect } from '../../dialect/pgLikeSqlDialect.js';
4
- import { MariaDialect } from '../../maria/mariaDialect.js';
5
- import { MySqlDialect } from '../../mysql/mysqlDialect.js';
6
1
  import { IndexDdl } from './indexDdl.js';
2
+ import { MsSqlIndexDdl } from './mssqlIndexDdl.js';
7
3
  import { MsSqlTableDdl } from './mssqlTableDdl.js';
8
- import { MariaIndexDdl, MySqlIndexDdl, MysqlLikeIndexDdl } from './mysqlIndexDdl.js';
4
+ import { MariaIndexDdl, MySqlIndexDdl } from './mysqlIndexDdl.js';
9
5
  import { CockroachIndexDdl, PgIndexDdl } from './pgIndexDdl.js';
10
6
  import { TableDdl } from './tableDdl.js';
11
7
  export { IndexDdl } from './indexDdl.js';
8
+ export { MsSqlIndexDdl } from './mssqlIndexDdl.js';
12
9
  export { MsSqlTableDdl } from './mssqlTableDdl.js';
13
10
  export { MariaIndexDdl, MySqlIndexDdl, MysqlLikeIndexDdl } from './mysqlIndexDdl.js';
14
11
  export { CockroachIndexDdl, PgIndexDdl } from './pgIndexDdl.js';
15
12
  export { TableDdl } from './tableDdl.js';
16
13
  /**
17
- * The index DDL a dialect gets, most specific first. `instanceof` rather than the `dialectName`
18
- * {@link tableDdlFor} reads, because each family's index DDL is typed to its dialect and the narrowing
19
- * is what hands it one. Anything else gets the portable form, which is SQLite's.
14
+ * Each engine's index DDL, by the `dialectName` a subclass inherits: by name, so this entry carries no
15
+ * dialect, and exhaustive, so a new engine has to name its own. SQLite's is the portable form.
20
16
  */
17
+ const INDEX_DDL = {
18
+ postgres: PgIndexDdl,
19
+ cockroachdb: CockroachIndexDdl,
20
+ mysql: MySqlIndexDdl,
21
+ mariadb: MariaIndexDdl,
22
+ mssql: MsSqlIndexDdl,
23
+ sqlite: IndexDdl,
24
+ };
21
25
  export function indexDdlFor(dialect) {
22
- if (dialect instanceof CockroachDialect) {
23
- return new CockroachIndexDdl(dialect);
24
- }
25
- if (dialect instanceof PgLikeSqlDialect) {
26
- return new PgIndexDdl(dialect);
27
- }
28
- if (dialect instanceof MySqlDialect) {
29
- return new MySqlIndexDdl(dialect);
30
- }
31
- if (dialect instanceof MariaDialect) {
32
- return new MariaIndexDdl(dialect);
33
- }
34
- if (dialect instanceof MysqlLikeSqlDialect) {
35
- return new MysqlLikeIndexDdl(dialect);
36
- }
37
- return new IndexDdl(dialect);
26
+ return new INDEX_DDL[dialect.dialectName](dialect);
38
27
  }
39
28
  /**
40
29
  * The table DDL a dialect gets: SQL Server's, or the portable form every other engine takes. By