uql-orm 0.56.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 (89) 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/cockroachdb/cockroachDialect.d.ts +5 -2
  5. package/dist/cockroachdb/cockroachDialect.js +2 -10
  6. package/dist/d1/d1SqliteDialect.d.ts +1 -0
  7. package/dist/d1/d1SqliteDialect.js +2 -0
  8. package/dist/dialect/abstractSqlDialect.d.ts +195 -32
  9. package/dist/dialect/abstractSqlDialect.js +406 -199
  10. package/dist/dialect/aliases.d.ts +10 -7
  11. package/dist/dialect/aliases.js +12 -7
  12. package/dist/dialect/hydrateColumn.d.ts +8 -2
  13. package/dist/dialect/hydrateColumn.js +33 -1
  14. package/dist/dialect/jsonSql.d.ts +13 -5
  15. package/dist/dialect/jsonSql.js +24 -7
  16. package/dist/dialect/mysqlLikeSqlDialect.d.ts +30 -2
  17. package/dist/dialect/mysqlLikeSqlDialect.js +56 -4
  18. package/dist/dialect/pgLikeSqlDialect.d.ts +20 -20
  19. package/dist/dialect/pgLikeSqlDialect.js +23 -48
  20. package/dist/dialect/pgVectorMetrics.d.ts +13 -0
  21. package/dist/dialect/pgVectorMetrics.js +17 -0
  22. package/dist/dialect/queryContext.d.ts +3 -7
  23. package/dist/dialect/queryContext.js +13 -8
  24. package/dist/dialect/queryJoins.d.ts +8 -4
  25. package/dist/dialect/queryJoins.js +25 -11
  26. package/dist/dialect/vectorSqlDialect.d.ts +2 -2
  27. package/dist/dialect/vectorSqlDialect.js +2 -3
  28. package/dist/entity/metadata/definition.js +3 -3
  29. package/dist/maria/mariaDialect.d.ts +13 -6
  30. package/dist/maria/mariaDialect.js +29 -9
  31. package/dist/migrate/cli.d.ts +2 -3
  32. package/dist/migrate/cli.js +2 -2
  33. package/dist/migrate/ddl/index.d.ts +1 -5
  34. package/dist/migrate/ddl/index.js +14 -25
  35. package/dist/migrate/ddl/indexDdl.d.ts +11 -2
  36. package/dist/migrate/ddl/indexDdl.js +17 -1
  37. package/dist/migrate/ddl/mssqlIndexDdl.d.ts +10 -0
  38. package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
  39. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +10 -17
  40. package/dist/migrate/ddl/mysqlIndexDdl.js +16 -27
  41. package/dist/migrate/ddl/pgIndexDdl.d.ts +18 -8
  42. package/dist/migrate/ddl/pgIndexDdl.js +29 -12
  43. package/dist/migrate/migrator.d.ts +2 -2
  44. package/dist/migrate/schemaGenerator.d.ts +3 -3
  45. package/dist/migrate/schemaGenerator.js +5 -7
  46. package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
  47. package/dist/mongo/mongoDialect.d.ts +30 -17
  48. package/dist/mongo/mongoDialect.js +143 -101
  49. package/dist/mongo/mongodbQuerier.d.ts +10 -17
  50. package/dist/mongo/mongodbQuerier.js +31 -105
  51. package/dist/mssql/mssqlDialect.d.ts +16 -0
  52. package/dist/mssql/mssqlDialect.js +26 -4
  53. package/dist/mysql/mysqlDialect.d.ts +2 -0
  54. package/dist/mysql/mysqlDialect.js +4 -0
  55. package/dist/querier/abstractQuerier.d.ts +20 -36
  56. package/dist/querier/abstractQuerier.js +35 -129
  57. package/dist/querier/abstractQuerierPool.d.ts +2 -2
  58. package/dist/querier/abstractSqlQuerier.d.ts +4 -17
  59. package/dist/querier/abstractSqlQuerier.js +34 -44
  60. package/dist/schema/canonicalType.js +4 -4
  61. package/dist/schema/indexDifferences.js +4 -4
  62. package/dist/schema/schemaASTBuilder.js +31 -2
  63. package/dist/schema/schemaASTDiffer.js +5 -5
  64. package/dist/sqlite/sqliteDialect.d.ts +20 -1
  65. package/dist/sqlite/sqliteDialect.js +38 -7
  66. package/dist/turso/tursoDialect.d.ts +2 -0
  67. package/dist/turso/tursoDialect.js +2 -0
  68. package/dist/type/config.d.ts +2 -2
  69. package/dist/type/dialect.d.ts +4 -5
  70. package/dist/type/entity.d.ts +2 -1
  71. package/dist/type/migratorDialect.d.ts +4 -0
  72. package/dist/type/querier.d.ts +6 -6
  73. package/dist/type/query.d.ts +25 -48
  74. package/dist/type/query.js +10 -5
  75. package/dist/type/queryAggregate.d.ts +10 -10
  76. package/dist/type/queryAggregate.js +1 -1
  77. package/dist/type/universalQuerier.d.ts +4 -4
  78. package/dist/util/dialect.util.d.ts +1 -1
  79. package/dist/util/field.util.d.ts +5 -0
  80. package/dist/util/field.util.js +19 -0
  81. package/dist/util/object.util.d.ts +2 -0
  82. package/dist/util/object.util.js +4 -0
  83. package/dist/util/relationQuery.util.d.ts +12 -65
  84. package/dist/util/relationQuery.util.js +27 -81
  85. package/dist/util/rowKey.util.d.ts +1 -11
  86. package/dist/util/rowKey.util.js +1 -13
  87. package/package.json +1 -1
  88. package/dist/querier/relationCount.d.ts +0 -16
  89. package/dist/querier/relationCount.js +0 -121
@@ -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
1
  import { getMeta, relationOf } from '../entity/index.js';
2
- import { getKeys, getRelationRequestSummary, isToManyRelation, parseRelationAtKey, } from '../util/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,15 +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
71
  const relation = relationOf(meta, key);
58
72
  const { query, required } = parseRelationAtKey(key, populate);
59
- const join = addJoin(joins, parent, key, relation, query, required, true);
60
- 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);
61
75
  }
62
76
  }
63
- function addSortJoins(joins, meta, sort, parent) {
77
+ function addSortJoins(joins, claimAlias, meta, sort, parent) {
64
78
  if (!sort) {
65
79
  return;
66
80
  }
@@ -72,8 +86,8 @@ function addSortJoins(joins, meta, sort, parent) {
72
86
  if (!relation || isToManyRelation(relation) || !isSortMap(value)) {
73
87
  continue;
74
88
  }
75
- const join = addJoin(joins, parent, key, relation, {}, false, false);
76
- 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);
77
91
  }
78
92
  }
79
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
@@ -337,9 +337,9 @@ function fillRelations(meta) {
337
337
  function fillOwningSide(at, meta, relKey, relOpts) {
338
338
  const relMeta = ensureMeta(relOpts.entity());
339
339
  if (relOpts.through) {
340
- // Both columns live on the junction, whatever the cardinality: `fillToManyThroughRelation`,
341
- // `deleteRelations` and every dialect read them as junction columns. A composite key contributes
342
- // 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.
343
343
  relOpts.references = [
344
344
  ...meta.ids.map((key) => ({ local: junctionColumn(meta, key), foreign: key })),
345
345
  ...relMeta.ids.map((key) => ({ local: junctionColumn(relMeta, key), foreign: key })),
@@ -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) {
@@ -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>;
@@ -308,7 +308,7 @@ function printDriftGroup(title, drifts, icon, showSuggestion) {
308
308
  console.log(` Expected: ${drift.expected}, Actual: ${drift.actual}`);
309
309
  }
310
310
  if (showSuggestion) {
311
- console.log(` → ${drift.suggestion}`);
311
+ console.log(` -> ${drift.suggestion}`);
312
312
  }
313
313
  }
314
314
  console.log('');
@@ -358,7 +358,7 @@ Configuration:
358
358
  Create a uql.config.ts or uql.config.js file in your project root.
359
359
  You can also specify a custom config path using --config or -c.
360
360
  The CLI requires pool.dialect (dialect id = pool.dialect.dialectName).
361
- See the repo README section "Driver → pool → dialect class".
361
+ See the repo README section "Driver -> pool -> dialect class".
362
362
 
363
363
  export default {
364
364
  pool: new PgQuerierPool({ ... }),
@@ -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
@@ -1,5 +1,5 @@
1
1
  import type { AbstractSqlDialect } from '../../dialect/abstractSqlDialect.js';
2
- import type { IndexType } from '../../schema/types.js';
2
+ import { type IndexType } from '../../schema/types.js';
3
3
  import { type IndexColumnSchema, type IndexFeature, type IndexJsonArray, type IndexSchema } from '../../type/index.js';
4
4
  /**
5
5
  * `CREATE INDEX` for SQL dialects: the statement and the fragments each engine spells differently.
@@ -20,6 +20,15 @@ export declare class IndexDdl<D extends AbstractSqlDialect = AbstractSqlDialect>
20
20
  * emitted: each of them is a hard error at the server, not a slower plan.
21
21
  */
22
22
  protected readonly indexFeatures: ReadonlySet<IndexFeature>;
23
+ /**
24
+ * Index types this dialect's `CREATE INDEX` takes. SQLite's grammar has no `USING` clause, so every
25
+ * type builds the plain index it has there, which is what lets an entity written for Postgres
26
+ * migrate unchanged. An engine that would reject a type narrows this, and the type is refused.
27
+ */
28
+ protected readonly indexTypes: ReadonlySet<IndexType>;
29
+ /** What to declare instead of a type this dialect lacks, appended to its refusal. */
30
+ protected readonly indexTypeHints: ReadonlyMap<IndexType, string>;
31
+ private assertIndexType;
23
32
  private assertIndexFeatures;
24
33
  /**
25
34
  * Index types this dialect spells as a keyword of their own (`FULLTEXT INDEX`, `VECTOR INDEX`)
@@ -51,7 +60,7 @@ export declare class IndexDdl<D extends AbstractSqlDialect = AbstractSqlDialect>
51
60
  protected indexInclude(_index: IndexSchema): string;
52
61
  /** ` USING <method>`, which SQLite's grammar has no place for at all. */
53
62
  protected indexAccessMethod(_index: IndexSchema): string;
54
- /** pgvector's ` WITH (m = ..., ef_construction = ..., lists = ...)`. */
63
+ /** What trails the columns: pgvector's ` WITH (m = ...)`, MySQL's ` USING btree`, MariaDB's ` M=8`. */
55
64
  protected indexTuning(_index: IndexSchema): string;
56
65
  /**
57
66
  * The partial-index predicate. Engines without one reject the index in {@link assertIndexFeatures}
@@ -1,4 +1,5 @@
1
1
  import { jsonTypeMode } from '../../dialect/jsonSql.js';
2
+ import { INDEX_TYPES } from '../../schema/types.js';
2
3
  import { INDEX_FEATURE_LABELS, } from '../../type/index.js';
3
4
  import { getKeys } from '../../util/index.js';
4
5
  /**
@@ -29,6 +30,7 @@ export class IndexDdl {
29
30
  this.dialect = dialect;
30
31
  }
31
32
  getCreateIndexStatement(tableName, index, opts = {}) {
33
+ this.assertIndexType(index);
32
34
  this.assertIndexFeatures(index);
33
35
  const unique = index.unique ? 'UNIQUE ' : '';
34
36
  const ifNotExists = (opts.ifNotExists ?? this.dialect.features.indexIfNotExists) ? 'IF NOT EXISTS ' : '';
@@ -47,6 +49,20 @@ export class IndexDdl {
47
49
  'partial',
48
50
  'jsonPath',
49
51
  ]);
52
+ /**
53
+ * Index types this dialect's `CREATE INDEX` takes. SQLite's grammar has no `USING` clause, so every
54
+ * type builds the plain index it has there, which is what lets an entity written for Postgres
55
+ * migrate unchanged. An engine that would reject a type narrows this, and the type is refused.
56
+ */
57
+ indexTypes = new Set(INDEX_TYPES);
58
+ /** What to declare instead of a type this dialect lacks, appended to its refusal. */
59
+ indexTypeHints = new Map();
60
+ assertIndexType(index) {
61
+ if (index.type && !this.indexTypes.has(index.type)) {
62
+ throw new TypeError(`${this.dialect.dialectName} has no ${index.type} index (index "${index.name}")` +
63
+ (this.indexTypeHints.get(index.type) ?? ''));
64
+ }
65
+ }
50
66
  assertIndexFeatures(index) {
51
67
  for (const feature of getKeys(INDEX_FEATURE_PROBES)) {
52
68
  if (INDEX_FEATURE_PROBES[feature](index) && !this.indexFeatures.has(feature)) {
@@ -111,7 +127,7 @@ export class IndexDdl {
111
127
  indexAccessMethod(_index) {
112
128
  return '';
113
129
  }
114
- /** pgvector's ` WITH (m = ..., ef_construction = ..., lists = ...)`. */
130
+ /** What trails the columns: pgvector's ` WITH (m = ...)`, MySQL's ` USING btree`, MariaDB's ` M=8`. */
115
131
  indexTuning(_index) {
116
132
  return '';
117
133
  }
@@ -0,0 +1,10 @@
1
+ import { IndexDdl } from './indexDdl.js';
2
+ /**
3
+ * SQL Server's `CREATE INDEX` is the portable form minus what 2025 rejects: an expression (Msg 16216),
4
+ * the subquery a JSON path compiles to (Msg 1046), and any type but the plain rowstore B-tree, since
5
+ * the index built in its place fails on a `VECTOR` or `nvarchar(max)` column (Msg 1978).
6
+ */
7
+ export declare class MsSqlIndexDdl extends IndexDdl {
8
+ protected readonly indexFeatures: Set<"expression" | "include" | "jsonArray" | "jsonPath" | "nullsOrder" | "opsClass" | "partial" | "prefixLength">;
9
+ protected readonly indexTypes: Set<"brin" | "btree" | "fulltext" | "gin" | "gist" | "hash" | "hnsw" | "ivfflat" | "vector" | "vectorSearch">;
10
+ }
@@ -0,0 +1,10 @@
1
+ import { IndexDdl } from './indexDdl.js';
2
+ /**
3
+ * SQL Server's `CREATE INDEX` is the portable form minus what 2025 rejects: an expression (Msg 16216),
4
+ * the subquery a JSON path compiles to (Msg 1046), and any type but the plain rowstore B-tree, since
5
+ * the index built in its place fails on a `VECTOR` or `nvarchar(max)` column (Msg 1978).
6
+ */
7
+ export class MsSqlIndexDdl extends IndexDdl {
8
+ indexFeatures = new Set(['partial']);
9
+ indexTypes = new Set(['btree']);
10
+ }
@@ -1,19 +1,13 @@
1
1
  import type { IndexType } from '../../schema/types.js';
2
2
  import type { IndexJsonArray, IndexSchema } from '../../type/index.js';
3
3
  import { IndexDdl } from './indexDdl.js';
4
- /** `CREATE INDEX ... USING btree`, plus the types this family spells as a keyword instead. */
4
+ /** `CREATE INDEX ... (cols) USING btree`, plus the types this family spells as a keyword instead. */
5
5
  export declare class MysqlLikeIndexDdl extends IndexDdl {
6
6
  protected readonly indexFeatures: Set<"expression" | "include" | "jsonArray" | "jsonPath" | "nullsOrder" | "opsClass" | "partial" | "prefixLength">;
7
+ protected readonly indexTypes: ReadonlySet<IndexType>;
7
8
  protected readonly indexTypeKeywords: ReadonlyMap<IndexType, string>;
8
- /**
9
- * ` USING btree|hash`, for the types this family does *not* spell as a keyword of its own. A vector
10
- * type that is neither is one this engine has no index for at all, refused here rather than
11
- * compiled into a ` USING hnsw` the server can only answer with a syntax error: which of them a
12
- * dialect *does* have is `indexTypeKeywords`, so declaring one there is all it takes to serve it.
13
- */
14
- protected indexAccessMethod(index: IndexSchema): string;
15
- /** What to do instead, appended to the refusal above. */
16
- protected readonly vectorIndexHint: string;
9
+ /** ` USING btree|hash` trails the columns: between the table and them, it is a syntax error here. */
10
+ protected indexTuning(index: IndexSchema): string;
17
11
  }
18
12
  export declare class MySqlIndexDdl extends MysqlLikeIndexDdl {
19
13
  /** The multi-valued index is the only JSON index MySQL has - see `IndexFeature` for why. */
@@ -25,12 +19,10 @@ export declare class MySqlIndexDdl extends MysqlLikeIndexDdl {
25
19
  */
26
20
  protected jsonArrayIndexExpr(escapedColumn: string, json: IndexJsonArray): string;
27
21
  /**
28
- * MySQL has no vector index of any kind, so one is refused rather than compiled to DDL the server
29
- * rejects: `USING hnsw` is a syntax error, and MariaDB's `VECTOR INDEX` is not MySQL syntax either.
30
- * Verified against 26.7, which does have `VECTOR` columns and `STRING_TO_VECTOR`, but no distance
31
- * function outside HeatWave - hence nothing to index for.
22
+ * MySQL 26.7 has `VECTOR` columns and `STRING_TO_VECTOR`, but no distance function outside
23
+ * HeatWave, hence no vector index to build: `USING hnsw` is a syntax error, `VECTOR INDEX` MariaDB's.
32
24
  */
33
- protected readonly vectorIndexHint = ". Vector search on MySQL needs HeatWave";
25
+ protected readonly indexTypeHints: Map<"brin" | "btree" | "fulltext" | "gin" | "gist" | "hash" | "hnsw" | "ivfflat" | "vector" | "vectorSearch", string>;
34
26
  }
35
27
  export declare class MariaIndexDdl extends MysqlLikeIndexDdl {
36
28
  /**
@@ -41,12 +33,13 @@ export declare class MariaIndexDdl extends MysqlLikeIndexDdl {
41
33
  protected readonly indexFeatures: Set<"expression" | "include" | "jsonArray" | "jsonPath" | "nullsOrder" | "opsClass" | "partial" | "prefixLength">;
42
34
  /** The family's, plus a vector index of its own: `CREATE VECTOR INDEX ... ON t (col)`, 11.7+. */
43
35
  protected readonly indexTypeKeywords: ReadonlyMap<IndexType, string>;
36
+ protected readonly indexTypes: Set<"brin" | "btree" | "fulltext" | "gin" | "gist" | "hash" | "hnsw" | "ivfflat" | "vector" | "vectorSearch">;
37
+ /** pgvector's names are not access methods it has; `vector` is its own keyword above. */
38
+ protected readonly indexTypeHints: Map<"brin" | "btree" | "fulltext" | "gin" | "gist" | "hash" | "hnsw" | "ivfflat" | "vector" | "vectorSearch", string>;
44
39
  /**
45
40
  * `M=n DISTANCE=metric`, trailing its `CREATE VECTOR INDEX`. The metric names are MariaDB's own
46
41
  * (`euclidean`, not `l2`), and an unsupported one throws rather than being dropped, which would
47
42
  * silently build the index on euclidean - its default - instead of what the entity asked for.
48
43
  */
49
44
  protected indexTuning(index: IndexSchema): string;
50
- /** `vector` is its own keyword above; pgvector's names are not access methods it has. */
51
- protected readonly vectorIndexHint = "; declare type: 'vector' instead";
52
45
  }
@@ -1,34 +1,21 @@
1
1
  import { jsonPath } from '../../dialect/jsonSql.js';
2
2
  import { MARIA_VECTOR_METRICS } from '../../maria/mariaVectorMetrics.js';
3
- import { isVectorIndexType, unsupportedVectorMetric } from '../../type/vector.js';
3
+ import { unsupportedVectorMetric, VECTOR_INDEX_TYPES } from '../../type/vector.js';
4
4
  import { IndexDdl } from './indexDdl.js';
5
5
  /**
6
6
  * A full-text index is its own keyword here (`CREATE FULLTEXT INDEX ... (cols)`); `USING fulltext` is
7
7
  * a syntax error, so it is the keyword that changes rather than the access method.
8
8
  */
9
9
  const MYSQL_LIKE_INDEX_KEYWORDS = new Map([['fulltext', 'FULLTEXT INDEX']]);
10
- /** `CREATE INDEX ... USING btree`, plus the types this family spells as a keyword instead. */
10
+ /** `CREATE INDEX ... (cols) USING btree`, plus the types this family spells as a keyword instead. */
11
11
  export class MysqlLikeIndexDdl extends IndexDdl {
12
12
  indexFeatures = new Set(['expression', 'prefixLength']);
13
+ indexTypes = new Set(['btree', 'hash', 'fulltext']);
13
14
  indexTypeKeywords = MYSQL_LIKE_INDEX_KEYWORDS;
14
- /**
15
- * ` USING btree|hash`, for the types this family does *not* spell as a keyword of its own. A vector
16
- * type that is neither is one this engine has no index for at all, refused here rather than
17
- * compiled into a ` USING hnsw` the server can only answer with a syntax error: which of them a
18
- * dialect *does* have is `indexTypeKeywords`, so declaring one there is all it takes to serve it.
19
- */
20
- indexAccessMethod(index) {
21
- const type = index.type;
22
- if (!type || this.indexTypeKeywords.has(type)) {
23
- return '';
24
- }
25
- if (isVectorIndexType(type)) {
26
- throw new TypeError(`${this.dialect.dialectName} has no ${type} index (index "${index.name}")${this.vectorIndexHint}`);
27
- }
28
- return ` USING ${type}`;
15
+ /** ` USING btree|hash` trails the columns: between the table and them, it is a syntax error here. */
16
+ indexTuning(index) {
17
+ return index.type && !this.indexTypeKeywords.has(index.type) ? ` USING ${index.type}` : '';
29
18
  }
30
- /** What to do instead, appended to the refusal above. */
31
- vectorIndexHint = '';
32
19
  }
33
20
  export class MySqlIndexDdl extends MysqlLikeIndexDdl {
34
21
  /** The multi-valued index is the only JSON index MySQL has - see `IndexFeature` for why. */
@@ -43,12 +30,10 @@ export class MySqlIndexDdl extends MysqlLikeIndexDdl {
43
30
  return `CAST(${source} AS ${arrayCastType(json)} ARRAY)`;
44
31
  }
45
32
  /**
46
- * MySQL has no vector index of any kind, so one is refused rather than compiled to DDL the server
47
- * rejects: `USING hnsw` is a syntax error, and MariaDB's `VECTOR INDEX` is not MySQL syntax either.
48
- * Verified against 26.7, which does have `VECTOR` columns and `STRING_TO_VECTOR`, but no distance
49
- * function outside HeatWave - hence nothing to index for.
33
+ * MySQL 26.7 has `VECTOR` columns and `STRING_TO_VECTOR`, but no distance function outside
34
+ * HeatWave, hence no vector index to build: `USING hnsw` is a syntax error, `VECTOR INDEX` MariaDB's.
50
35
  */
51
- vectorIndexHint = '. Vector search on MySQL needs HeatWave';
36
+ indexTypeHints = new Map(VECTOR_INDEX_TYPES.map((type) => [type, '. Vector search on MySQL needs HeatWave']));
52
37
  }
53
38
  export class MariaIndexDdl extends MysqlLikeIndexDdl {
54
39
  /**
@@ -62,13 +47,19 @@ export class MariaIndexDdl extends MysqlLikeIndexDdl {
62
47
  ...MYSQL_LIKE_INDEX_KEYWORDS,
63
48
  ['vector', 'VECTOR INDEX'],
64
49
  ]);
50
+ indexTypes = new Set(['btree', 'hash', 'fulltext', 'vector']);
51
+ /** pgvector's names are not access methods it has; `vector` is its own keyword above. */
52
+ indexTypeHints = new Map([
53
+ ['hnsw', "; declare type: 'vector' instead"],
54
+ ['ivfflat', "; declare type: 'vector' instead"],
55
+ ]);
65
56
  /**
66
57
  * `M=n DISTANCE=metric`, trailing its `CREATE VECTOR INDEX`. The metric names are MariaDB's own
67
58
  * (`euclidean`, not `l2`), and an unsupported one throws rather than being dropped, which would
68
59
  * silently build the index on euclidean - its default - instead of what the entity asked for.
69
60
  */
70
61
  indexTuning(index) {
71
- let tuning = index.m === undefined ? '' : ` M=${index.m}`;
62
+ let tuning = super.indexTuning(index) + (index.m === undefined ? '' : ` M=${index.m}`);
72
63
  if (index.distance) {
73
64
  const metric = MARIA_VECTOR_METRICS.get(index.distance);
74
65
  if (!metric) {
@@ -78,8 +69,6 @@ export class MariaIndexDdl extends MysqlLikeIndexDdl {
78
69
  }
79
70
  return tuning;
80
71
  }
81
- /** `vector` is its own keyword above; pgvector's names are not access methods it has. */
82
- vectorIndexHint = "; declare type: 'vector' instead";
83
72
  }
84
73
  /**
85
74
  * MySQL's `CAST(... AS <type> ARRAY)` targets, the closed list its multi-valued index takes: no