uql-orm 0.21.0 → 0.22.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 (142) hide show
  1. package/README.md +5 -4
  2. package/dist/bunSql/bunSqlCockroachDialect.d.ts +2 -2
  3. package/dist/bunSql/bunSqlCockroachDialect.js +4 -10
  4. package/dist/bunSql/bunSqlPostgresDialect.d.ts +2 -2
  5. package/dist/bunSql/bunSqlPostgresDialect.js +4 -10
  6. package/dist/cockroachdb/cockroachDialect.d.ts +20 -4
  7. package/dist/cockroachdb/cockroachDialect.js +33 -18
  8. package/dist/d1/d1Querier.d.ts +4 -3
  9. package/dist/d1/d1Querier.js +0 -3
  10. package/dist/d1/d1SqliteDialect.d.ts +7 -0
  11. package/dist/d1/d1SqliteDialect.js +9 -0
  12. package/dist/dialect/abstractDialect.d.ts +8 -3
  13. package/dist/dialect/abstractDialect.js +9 -4
  14. package/dist/dialect/abstractSqlDialect.d.ts +35 -7
  15. package/dist/dialect/abstractSqlDialect.js +76 -35
  16. package/dist/dialect/index.d.ts +1 -0
  17. package/dist/dialect/index.js +1 -0
  18. package/dist/dialect/indexSqlDialect.d.ts +57 -0
  19. package/dist/dialect/indexSqlDialect.js +101 -0
  20. package/dist/dialect/mysqlLikeSqlDialect.d.ts +16 -1
  21. package/dist/dialect/mysqlLikeSqlDialect.js +32 -1
  22. package/dist/dialect/pgLikeSqlDialect.d.ts +33 -15
  23. package/dist/dialect/pgLikeSqlDialect.js +90 -56
  24. package/dist/dialect/vectorCast.d.ts +21 -0
  25. package/dist/dialect/vectorCast.js +28 -0
  26. package/dist/dialect/vectorSqlDialect.d.ts +10 -3
  27. package/dist/dialect/vectorSqlDialect.js +19 -8
  28. package/dist/entity/decorator/index-decorator.d.ts +2 -9
  29. package/dist/entity/metadata/definition.d.ts +5 -4
  30. package/dist/entity/metadata/definition.js +4 -3
  31. package/dist/libsql/libsqlDialect.d.ts +12 -2
  32. package/dist/libsql/libsqlDialect.js +14 -2
  33. package/dist/libsql/libsqlQuerier.d.ts +9 -22
  34. package/dist/libsql/libsqlQuerier.js +9 -85
  35. package/dist/libsql/libsqlQuerierPool.d.ts +10 -8
  36. package/dist/libsql/libsqlQuerierPool.js +17 -15
  37. package/dist/maria/mariaDialect.d.ts +26 -4
  38. package/dist/maria/mariaDialect.js +54 -15
  39. package/dist/migrate/builder/migrationBuilder.d.ts +9 -29
  40. package/dist/migrate/builder/migrationBuilder.js +58 -76
  41. package/dist/migrate/builder/splitSqlStatements.d.ts +0 -2
  42. package/dist/migrate/builder/splitSqlStatements.js +0 -2
  43. package/dist/migrate/builder/tableBuilder.d.ts +12 -2
  44. package/dist/migrate/builder/tableBuilder.js +71 -119
  45. package/dist/migrate/builder/types.d.ts +30 -41
  46. package/dist/migrate/cli.d.ts +3 -2
  47. package/dist/migrate/cli.js +21 -48
  48. package/dist/migrate/codegen/entityCodeGenerator.d.ts +0 -4
  49. package/dist/migrate/codegen/entityCodeGenerator.js +5 -63
  50. package/dist/migrate/codegen/fieldOptionsSource.d.ts +10 -0
  51. package/dist/migrate/codegen/fieldOptionsSource.js +55 -0
  52. package/dist/migrate/codegen/index.d.ts +0 -2
  53. package/dist/migrate/codegen/index.js +0 -4
  54. package/dist/migrate/generator/indexNodeToSchema.d.ts +9 -0
  55. package/dist/migrate/generator/indexNodeToSchema.js +14 -0
  56. package/dist/migrate/generator/mongoCommand.d.ts +57 -0
  57. package/dist/migrate/generator/mongoCommand.js +26 -0
  58. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +9 -20
  59. package/dist/migrate/generator/mongoSchemaGenerator.js +40 -82
  60. package/dist/migrate/index.d.ts +0 -1
  61. package/dist/migrate/index.js +0 -1
  62. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +25 -10
  63. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +46 -32
  64. package/dist/migrate/introspection/baseSqlIntrospector.d.ts +0 -1
  65. package/dist/migrate/introspection/baseSqlIntrospector.js +3 -12
  66. package/dist/migrate/introspection/mongoIntrospector.js +2 -2
  67. package/dist/migrate/introspection/mysqlIntrospector.d.ts +5 -8
  68. package/dist/migrate/introspection/mysqlIntrospector.js +4 -10
  69. package/dist/migrate/introspection/postgresIntrospector.d.ts +5 -8
  70. package/dist/migrate/introspection/postgresIntrospector.js +4 -10
  71. package/dist/migrate/introspection/sqliteIntrospector.d.ts +6 -5
  72. package/dist/migrate/introspection/sqliteIntrospector.js +26 -18
  73. package/dist/migrate/migrator.d.ts +23 -3
  74. package/dist/migrate/migrator.js +57 -69
  75. package/dist/migrate/schemaGenerator.d.ts +26 -37
  76. package/dist/migrate/schemaGenerator.js +69 -174
  77. package/dist/migrate/schemaGeneratorAsync.d.ts +2 -2
  78. package/dist/migrate/schemaGeneratorAsync.js +3 -3
  79. package/dist/migrate/storage/databaseStorage.js +4 -4
  80. package/dist/mongo/mongoDialect.d.ts +2 -0
  81. package/dist/mongo/mongoDialect.js +14 -3
  82. package/dist/mysql/mysqlDialect.d.ts +8 -0
  83. package/dist/mysql/mysqlDialect.js +13 -0
  84. package/dist/schema/canonicalType.d.ts +14 -2
  85. package/dist/schema/canonicalType.js +41 -31
  86. package/dist/schema/index.d.ts +1 -1
  87. package/dist/schema/schemaASTBuilder.d.ts +8 -1
  88. package/dist/schema/schemaASTBuilder.js +56 -47
  89. package/dist/schema/types.d.ts +10 -37
  90. package/dist/sqlite/abstractSqliteQuerier.d.ts +39 -0
  91. package/dist/sqlite/abstractSqliteQuerier.js +44 -0
  92. package/dist/sqlite/bunSqliteAdapter.bun.d.ts +26 -0
  93. package/dist/sqlite/bunSqliteAdapter.bun.js +25 -0
  94. package/dist/sqlite/hranaQuerier.d.ts +54 -0
  95. package/dist/sqlite/hranaQuerier.js +90 -0
  96. package/dist/sqlite/hranaQuerierPool.d.ts +21 -0
  97. package/dist/sqlite/hranaQuerierPool.js +25 -0
  98. package/dist/sqlite/index.d.ts +2 -1
  99. package/dist/sqlite/index.js +2 -1
  100. package/dist/sqlite/sqliteDialect.d.ts +18 -4
  101. package/dist/sqlite/sqliteDialect.js +24 -24
  102. package/dist/sqlite/sqliteQuerier.d.ts +16 -9
  103. package/dist/sqlite/sqliteQuerier.js +4 -27
  104. package/dist/sqlite/sqliteQuerierPool.d.ts +14 -4
  105. package/dist/sqlite/sqliteQuerierPool.js +16 -5
  106. package/dist/turso/index.d.ts +3 -0
  107. package/dist/turso/index.js +3 -0
  108. package/dist/turso/local.d.ts +3 -0
  109. package/dist/turso/local.js +3 -0
  110. package/dist/turso/tursoDialect.d.ts +15 -0
  111. package/dist/turso/tursoDialect.js +18 -0
  112. package/dist/turso/tursoLocalQuerier.d.ts +25 -0
  113. package/dist/turso/tursoLocalQuerier.js +20 -0
  114. package/dist/turso/tursoLocalQuerierPool.d.ts +32 -0
  115. package/dist/turso/tursoLocalQuerierPool.js +39 -0
  116. package/dist/turso/tursoQuerier.d.ts +10 -0
  117. package/dist/turso/tursoQuerier.js +10 -0
  118. package/dist/turso/tursoQuerierPool.d.ts +39 -0
  119. package/dist/turso/tursoQuerierPool.js +38 -0
  120. package/dist/type/config.d.ts +0 -6
  121. package/dist/type/dialect.d.ts +12 -4
  122. package/dist/type/dialect.js +7 -1
  123. package/dist/type/entity.d.ts +66 -5
  124. package/dist/type/migration.d.ts +32 -23
  125. package/dist/type/vector.d.ts +5 -2
  126. package/dist/util/index.d.ts +1 -0
  127. package/dist/util/index.js +1 -0
  128. package/dist/util/indexColumn.util.d.ts +6 -0
  129. package/dist/util/indexColumn.util.js +26 -0
  130. package/package.json +16 -3
  131. package/dist/migrate/codegen/entityMerger.d.ts +0 -114
  132. package/dist/migrate/codegen/entityMerger.js +0 -293
  133. package/dist/migrate/codegen/migrationCodeGenerator.d.ts +0 -61
  134. package/dist/migrate/codegen/migrationCodeGenerator.js +0 -355
  135. package/dist/migrate/generator/index.d.ts +0 -2
  136. package/dist/migrate/generator/index.js +0 -2
  137. package/dist/migrate/sync/index.d.ts +0 -6
  138. package/dist/migrate/sync/index.js +0 -6
  139. package/dist/migrate/sync/schemaSync.d.ts +0 -131
  140. package/dist/migrate/sync/schemaSync.js +0 -259
  141. package/dist/sqlite/betterSqlite3Dialect.d.ts +0 -9
  142. package/dist/sqlite/betterSqlite3Dialect.js +0 -9
@@ -0,0 +1,57 @@
1
+ import { type IndexColumnSchema, type IndexFeature, type IndexSchema } from '../type/index.js';
2
+ import { VectorSqlDialect } from './vectorSqlDialect.js';
3
+ /**
4
+ * `CREATE INDEX` for SQL dialects: the statement and the fragments each engine spells differently.
5
+ *
6
+ * A layer of its own for the same reason as {@link VectorSqlDialect} below it - it needs only
7
+ * `escapeId` and `features.indexIfNotExists` from the SQL dialect above, so keeping it here spares
8
+ * that 2000-line class thirteen more members. What each engine can express at all is data
9
+ * ({@link indexFeatures}), validated once, rather than a throw per feature per dialect.
10
+ */
11
+ export declare abstract class IndexSqlDialect extends VectorSqlDialect {
12
+ /**
13
+ * The `CREATE INDEX` statement for this dialect. The form here is the portable one, which SQLite
14
+ * (and so libSQL, Turso and D1) takes verbatim: no access-method clause, no operator classes, no
15
+ * tuning parameters. Dialects that have those override the fragments below rather than this.
16
+ */
17
+ getCreateIndexStatement(tableName: string, index: IndexSchema, opts?: {
18
+ ifNotExists?: boolean;
19
+ }): string;
20
+ /**
21
+ * Index features this dialect can express. Everything here is supported by at least one engine and
22
+ * refused by at least one other, so an index asking for a missing one is rejected rather than
23
+ * emitted: each of them is a hard error at the server, not a slower plan.
24
+ */
25
+ protected readonly indexFeatures: ReadonlySet<IndexFeature>;
26
+ private assertIndexFeatures;
27
+ /**
28
+ * The column-level declaration for a vector index that lives inside `CREATE TABLE` rather than in
29
+ * its own statement, which the `inlineVectorIndex` feature flags. Only MariaDB has one.
30
+ */
31
+ getInlineVectorIndexDeclaration(index: IndexSchema): string;
32
+ /** Only CockroachDB's native vector index replaces the `INDEX` keyword. */
33
+ protected indexKeyword(_index: IndexSchema): string;
34
+ /** One index entry: what is indexed, its operator class if any, then its stored order. */
35
+ protected indexColumn(entry: IndexColumnSchema, index: IndexSchema): string;
36
+ /**
37
+ * A quoted column, optionally prefix-limited, or an expression in its own parentheses - the form
38
+ * `((lower("email")))` that MySQL requires and Postgres, CockroachDB and SQLite all accept, so one
39
+ * rendering serves every engine that has expression indexes.
40
+ */
41
+ protected indexColumnTarget(entry: IndexColumnSchema): string;
42
+ /** Postgres-wire dialects put a vector or user-declared operator class here. */
43
+ protected indexColumnOpsClass(_entry: IndexColumnSchema, _index: IndexSchema): string;
44
+ /** `ASC` is every engine's default, so only `DESC` is worth emitting. */
45
+ protected indexColumnOrder(entry: IndexColumnSchema): string;
46
+ /** ` INCLUDE (...)`: non-key columns stored for index-only scans. Postgres-wire only. */
47
+ protected indexInclude(_index: IndexSchema): string;
48
+ /** ` USING <method>`, which SQLite's grammar has no place for at all. */
49
+ protected indexAccessMethod(_index: IndexSchema): string;
50
+ /** pgvector's ` WITH (m = ..., ef_construction = ..., lists = ...)`. */
51
+ protected indexTuning(_index: IndexSchema): string;
52
+ /**
53
+ * The partial-index predicate. Dialects without partial indexes throw instead of dropping it:
54
+ * silently widening a partial unique index changes which rows the database accepts.
55
+ */
56
+ protected indexPredicate(index: IndexSchema): string;
57
+ }
@@ -0,0 +1,101 @@
1
+ import { INDEX_FEATURE_LABELS } from '../type/index.js';
2
+ import { VectorSqlDialect } from './vectorSqlDialect.js';
3
+ /**
4
+ * `CREATE INDEX` for SQL dialects: the statement and the fragments each engine spells differently.
5
+ *
6
+ * A layer of its own for the same reason as {@link VectorSqlDialect} below it - it needs only
7
+ * `escapeId` and `features.indexIfNotExists` from the SQL dialect above, so keeping it here spares
8
+ * that 2000-line class thirteen more members. What each engine can express at all is data
9
+ * ({@link indexFeatures}), validated once, rather than a throw per feature per dialect.
10
+ */
11
+ export class IndexSqlDialect extends VectorSqlDialect {
12
+ /**
13
+ * The `CREATE INDEX` statement for this dialect. The form here is the portable one, which SQLite
14
+ * (and so libSQL, Turso and D1) takes verbatim: no access-method clause, no operator classes, no
15
+ * tuning parameters. Dialects that have those override the fragments below rather than this.
16
+ */
17
+ getCreateIndexStatement(tableName, index, opts = {}) {
18
+ this.assertIndexFeatures(index);
19
+ const unique = index.unique ? 'UNIQUE ' : '';
20
+ const ifNotExists = (opts.ifNotExists ?? this.features.indexIfNotExists) ? 'IF NOT EXISTS ' : '';
21
+ const columns = index.columns.map((entry) => this.indexColumn(entry, index)).join(', ');
22
+ return (`CREATE ${unique}${this.indexKeyword(index)} ${ifNotExists}${this.escapeId(index.name)} ` +
23
+ `ON ${this.escapeId(tableName)}${this.indexAccessMethod(index)} (${columns})` +
24
+ `${this.indexInclude(index)}${this.indexTuning(index)}${this.indexPredicate(index)};`);
25
+ }
26
+ /**
27
+ * Index features this dialect can express. Everything here is supported by at least one engine and
28
+ * refused by at least one other, so an index asking for a missing one is rejected rather than
29
+ * emitted: each of them is a hard error at the server, not a slower plan.
30
+ */
31
+ indexFeatures = new Set(['expression']);
32
+ assertIndexFeatures(index) {
33
+ const requested = [
34
+ ['expression', index.columns.some((entry) => entry.expression)],
35
+ ['prefixLength', index.columns.some((entry) => entry.length !== undefined)],
36
+ ['nullsOrder', index.columns.some((entry) => entry.nulls !== undefined)],
37
+ ['opsClass', index.columns.some((entry) => entry.opsClass !== undefined)],
38
+ ['include', Boolean(index.include?.length)],
39
+ ];
40
+ for (const [feature, needed] of requested) {
41
+ if (needed && !this.indexFeatures.has(feature)) {
42
+ throw new TypeError(`${this.dialectName} does not support ${INDEX_FEATURE_LABELS[feature]} (index "${index.name}")`);
43
+ }
44
+ }
45
+ }
46
+ /**
47
+ * The column-level declaration for a vector index that lives inside `CREATE TABLE` rather than in
48
+ * its own statement, which the `inlineVectorIndex` feature flags. Only MariaDB has one.
49
+ */
50
+ getInlineVectorIndexDeclaration(index) {
51
+ throw new TypeError(`${this.dialectName} has no inline vector index (index "${index.name}")`);
52
+ }
53
+ /** Only CockroachDB's native vector index replaces the `INDEX` keyword. */
54
+ indexKeyword(_index) {
55
+ return 'INDEX';
56
+ }
57
+ /** One index entry: what is indexed, its operator class if any, then its stored order. */
58
+ indexColumn(entry, index) {
59
+ return `${this.indexColumnTarget(entry)}${this.indexColumnOpsClass(entry, index)}${this.indexColumnOrder(entry)}`;
60
+ }
61
+ /**
62
+ * A quoted column, optionally prefix-limited, or an expression in its own parentheses - the form
63
+ * `((lower("email")))` that MySQL requires and Postgres, CockroachDB and SQLite all accept, so one
64
+ * rendering serves every engine that has expression indexes.
65
+ */
66
+ indexColumnTarget(entry) {
67
+ if (entry.expression) {
68
+ return `(${entry.column})`;
69
+ }
70
+ const column = this.escapeId(entry.column);
71
+ return entry.length === undefined ? column : `${column}(${entry.length})`;
72
+ }
73
+ /** Postgres-wire dialects put a vector or user-declared operator class here. */
74
+ indexColumnOpsClass(_entry, _index) {
75
+ return '';
76
+ }
77
+ /** `ASC` is every engine's default, so only `DESC` is worth emitting. */
78
+ indexColumnOrder(entry) {
79
+ const order = entry.order === 'desc' ? ' DESC' : '';
80
+ return entry.nulls ? `${order} NULLS ${entry.nulls.toUpperCase()}` : order;
81
+ }
82
+ /** ` INCLUDE (...)`: non-key columns stored for index-only scans. Postgres-wire only. */
83
+ indexInclude(_index) {
84
+ return '';
85
+ }
86
+ /** ` USING <method>`, which SQLite's grammar has no place for at all. */
87
+ indexAccessMethod(_index) {
88
+ return '';
89
+ }
90
+ /** pgvector's ` WITH (m = ..., ef_construction = ..., lists = ...)`. */
91
+ indexTuning(_index) {
92
+ return '';
93
+ }
94
+ /**
95
+ * The partial-index predicate. Dialects without partial indexes throw instead of dropping it:
96
+ * silently widening a partial unique index changes which rows the database accepts.
97
+ */
98
+ indexPredicate(index) {
99
+ return index.where ? ` WHERE ${index.where}` : '';
100
+ }
101
+ }
@@ -1,4 +1,4 @@
1
- import type { DialectFeatures, FieldOptions, InsertIdSource, QueryContext, QuerySizeComparisonOps } from '../type/index.js';
1
+ import type { DialectFeatures, EntityMeta, FieldOptions, IndexFeature, IndexSchema, InsertIdSource, QueryContext, QuerySizeComparisonOps, QueryTextSearchOptions, Type } from '../type/index.js';
2
2
  import { AbstractSqlDialect } from './abstractSqlDialect.js';
3
3
  /**
4
4
  * Shared JSON-array / JSON-object operator implementation between MySQL and MariaDB.
@@ -28,6 +28,21 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
28
28
  readonly insertIdSource: InsertIdSource;
29
29
  readonly maxBindValues: number;
30
30
  escape(value: unknown): string;
31
+ /**
32
+ * `MATCH(cols) AGAINST(?)`, which needs a `FULLTEXT` index over exactly those columns: without one
33
+ * the server answers "Can't find FULLTEXT index matching the column list". Declare it with
34
+ * `@Index([...], { type: 'fulltext' })`.
35
+ */
36
+ protected appendTextSearch<E>(ctx: QueryContext, _entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
37
+ /**
38
+ * A full-text index is its own keyword here (`CREATE FULLTEXT INDEX ... (cols)`); `USING fulltext`
39
+ * is a syntax error, so it is the keyword that changes rather than the access method.
40
+ */
41
+ protected indexKeyword(index: IndexSchema): string;
42
+ protected readonly indexFeatures: Set<IndexFeature>;
43
+ protected indexAccessMethod(index: IndexSchema): string;
44
+ /** Neither MySQL nor MariaDB has partial indexes, and quietly widening one changes which rows it rejects. */
45
+ protected indexPredicate(index: IndexSchema): string;
31
46
  protected numericCast(expr: string): string;
32
47
  protected ilikeExpr(f: string, ph: string): string;
33
48
  protected neExpr(field: string, ph: string): string;
@@ -1,3 +1,4 @@
1
+ import { getFieldKeys } from '../util/index.js';
1
2
  import { escapeMysqlSqlLiteral, escapeSingleQuotes } from '../util/sqlLiteral.js';
2
3
  import { AbstractSqlDialect } from './abstractSqlDialect.js';
3
4
  import { JSON_PULL_ALIAS, jsonAssignCall, jsonPath, jsonRemoveCall, jsonSetTarget } from './jsonSql.js';
@@ -23,7 +24,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
23
24
  renameColumn: true,
24
25
  foreignKeyAlter: true,
25
26
  columnComment: true,
26
- vectorIndexStyle: 'inline',
27
+ inlineVectorIndex: false,
27
28
  vectorSupportsLength: false,
28
29
  supportsTimestamptz: false,
29
30
  defaultStringAsText: false,
@@ -47,6 +48,36 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
47
48
  escape(value) {
48
49
  return escapeMysqlSqlLiteral(value);
49
50
  }
51
+ /**
52
+ * `MATCH(cols) AGAINST(?)`, which needs a `FULLTEXT` index over exactly those columns: without one
53
+ * the server answers "Can't find FULLTEXT index matching the column list". Declare it with
54
+ * `@Index([...], { type: 'fulltext' })`.
55
+ */
56
+ appendTextSearch(ctx, _entity, meta, search) {
57
+ const searchFields = search.$fields ?? getFieldKeys(meta.fields);
58
+ const columns = searchFields.map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])));
59
+ ctx.append(`MATCH(${columns.join(', ')}) AGAINST(`);
60
+ ctx.addValue(search.$value);
61
+ ctx.append(')');
62
+ }
63
+ /**
64
+ * A full-text index is its own keyword here (`CREATE FULLTEXT INDEX ... (cols)`); `USING fulltext`
65
+ * is a syntax error, so it is the keyword that changes rather than the access method.
66
+ */
67
+ indexKeyword(index) {
68
+ return index.type === 'fulltext' ? 'FULLTEXT INDEX' : super.indexKeyword(index);
69
+ }
70
+ indexFeatures = new Set(['expression', 'prefixLength']);
71
+ indexAccessMethod(index) {
72
+ return index.type && index.type !== 'fulltext' ? ` USING ${index.type}` : '';
73
+ }
74
+ /** Neither MySQL nor MariaDB has partial indexes, and quietly widening one changes which rows it rejects. */
75
+ indexPredicate(index) {
76
+ if (index.where) {
77
+ throw new TypeError(`${this.dialectName} does not support partial indexes (index "${index.name}" declares a "where" condition)`);
78
+ }
79
+ return '';
80
+ }
50
81
  numericCast(expr) {
51
82
  return `CAST(${expr} AS DECIMAL)`;
52
83
  }
@@ -1,4 +1,4 @@
1
- import { type DialectFeatures, type EntityMeta, type FieldOptions, type JsonColumnType, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryOptions, type QuerySizeComparisonOps, type QueryVectorSearch, type Type, type VectorDistance } from '../type/index.js';
1
+ import { type DialectFeatures, type EntityMeta, type FieldOptions, type IndexColumnSchema, type IndexFeature, type IndexSchema, type JsonColumnType, type QueryConflictPaths, type QueryContext, type QuerySizeComparisonOps, type QueryTextSearchOptions, type QueryVectorSearch, type Type, type VectorDistance } from '../type/index.js';
2
2
  import { AbstractSqlDialect } from './abstractSqlDialect.js';
3
3
  /**
4
4
  * Shared AST/quoting/JSONB/full-text-search/vector-search implementation between Postgres and
@@ -21,10 +21,31 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
21
21
  readonly alterColumnStrategy = "separate-clauses";
22
22
  readonly insertIdSource = "returning";
23
23
  readonly maxBindValues: number;
24
- readonly vectorOpsClass: ReadonlyMap<VectorDistance, string> | undefined;
24
+ /**
25
+ * Each metric's pgvector distance operator and the operator-class suffix its index takes, in one
26
+ * place so a dialect cannot end up with the operator but not the opclass. The key set is the single
27
+ * source of truth for which metrics the dialect supports at all: CockroachDB narrows it to three.
28
+ */
29
+ readonly vectorMetrics: ReadonlyMap<VectorDistance, {
30
+ op: string;
31
+ opsSuffix: string;
32
+ }>;
25
33
  normalizeValue(value: unknown): unknown;
34
+ /** pgvector's own index types; CockroachDB's native one widens this. */
35
+ protected isVectorIndex(index: IndexSchema): boolean;
36
+ protected indexAccessMethod(index: IndexSchema): string;
37
+ protected readonly indexFeatures: Set<IndexFeature>;
38
+ /**
39
+ * A vector index's operator class is named `{type}_{metric}_ops`: an index on a `halfvec` column
40
+ * needs `halfvec_cosine_ops`, and `vector_cosine_ops` there is rejected outright. An unsupported
41
+ * distance throws rather than being omitted, since a bare `USING hnsw ("embedding")` would build
42
+ * with the dialect's default metric instead of the one requested, with nothing signalling it.
43
+ * Everything else takes the operator class the entry declares, e.g. `jsonb_path_ops` for GIN.
44
+ */
45
+ protected indexColumnOpsClass(entry: IndexColumnSchema, index: IndexSchema): string;
46
+ protected indexInclude(index: IndexSchema): string;
47
+ protected indexTuning(index: IndexSchema): string;
26
48
  placeholder(index: number): string;
27
- insert<E>(ctx: QueryContext, entity: Type<E>, payload: E | E[], opts?: QueryOptions): void;
28
49
  upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[]): void;
29
50
  /**
30
51
  * Shared `INSERT ... ON CONFLICT (...) DO UPDATE/NOTHING RETURNING ...` builder, assembling
@@ -35,11 +56,11 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
35
56
  */
36
57
  protected buildUpsertOnConflict<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[], extraReturning?: string): void;
37
58
  /**
38
- * Full-text search: `to_tsvector(...) @@ websearch_to_tsquery(...)`. `websearch_to_tsquery` takes
39
- * free-form user input (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike
40
- * `to_tsquery`, which rejects anything unparseable - including a plain two-word search.
59
+ * `to_tsvector(...) @@ websearch_to_tsquery(...)`. `websearch_to_tsquery` takes free-form user input
60
+ * (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `to_tsquery`, which
61
+ * rejects anything unparseable - including a plain two-word search.
41
62
  */
42
- compare<E>(ctx: QueryContext, entity: Type<E>, key: string, val: unknown, opts?: QueryComparisonOptions): void;
63
+ protected appendTextSearch<E>(ctx: QueryContext, _entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
43
64
  protected jsonAll(ctx: QueryContext, jsonField: string, value: unknown): string;
44
65
  protected jsonSize(ctx: QueryContext, jsonField: string, value: number | QuerySizeComparisonOps): string;
45
66
  /**
@@ -54,7 +75,11 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
54
75
  protected formatIn(ctx: QueryContext, values: unknown[], negate: boolean): string;
55
76
  protected numericCast(expr: string): string;
56
77
  protected appendJsonValue(ctx: QueryContext, value: unknown, type: JsonColumnType): void;
57
- protected appendVectorValue(ctx: QueryContext, value: readonly unknown[]): void;
78
+ /**
79
+ * pgvector needs the cast to pick the right type, and `sparsevec` needs its own literal: the dense
80
+ * `[1,0,2]` every other type takes is rejected as "invalid input syntax for type sparsevec".
81
+ */
82
+ protected appendVectorValue(ctx: QueryContext, value: readonly unknown[], field?: FieldOptions): void;
58
83
  /**
59
84
  * `create_if_missing => false` keeps an absent key (and a NULL column) untouched; `WITH
60
85
  * ORDINALITY` keeps the surviving elements in their original order.
@@ -70,13 +95,6 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
70
95
  * Helper to add a JSON value to context with appropriate stringification and cast.
71
96
  */
72
97
  private jsonVal;
73
- /**
74
- * pgvector distance operators. Not every dialect supports every metric - see
75
- * {@link CockroachDialect.vectorOpsClass} for which ones each dialect actually has; the
76
- * `vectorOpsClass` key set (checked in `appendVectorSort` below via `vectorOpsClass.get`)
77
- * is the single source of truth for that, not this map.
78
- */
79
- private static readonly VECTOR_OPS;
80
98
  /** Emit a pgvector-style distance expression: `"col" <op> $N::<vectorType>`. */
81
99
  protected appendVectorSort<E>(ctx: QueryContext, meta: EntityMeta<E>, key: string, search: QueryVectorSearch): void;
82
100
  }
@@ -3,6 +3,7 @@ import { QueryRaw, } from '../type/index.js';
3
3
  import { escapeSingleQuotes } from '../util/sqlLiteral.js';
4
4
  import { AbstractSqlDialect } from './abstractSqlDialect.js';
5
5
  import { JSON_PULL_ALIAS, jsonSetTarget } from './jsonSql.js';
6
+ import { resolveVectorCast, toSparsevecLiteral } from './vectorCast.js';
6
7
  /**
7
8
  * Shared AST/quoting/JSONB/full-text-search/vector-search implementation between Postgres and
8
9
  * CockroachDB (wire- and SQL-compatible for everything below, including `to_tsvector`/`to_tsquery`
@@ -24,7 +25,7 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
24
25
  renameColumn: true,
25
26
  foreignKeyAlter: true,
26
27
  columnComment: false,
27
- vectorIndexStyle: 'create',
28
+ inlineVectorIndex: false,
28
29
  vectorSupportsLength: true,
29
30
  supportsTimestamptz: true,
30
31
  defaultStringAsText: true,
@@ -42,12 +43,16 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
42
43
  alterColumnStrategy = 'separate-clauses';
43
44
  insertIdSource = 'returning';
44
45
  maxBindValues = 65535;
45
- vectorOpsClass = new Map([
46
- ['cosine', 'vector_cosine_ops'],
47
- ['l2', 'vector_l2_ops'],
48
- ['inner', 'vector_ip_ops'],
49
- ['l1', 'vector_l1_ops'],
50
- ['hamming', 'bit_hamming_ops'],
46
+ /**
47
+ * Each metric's pgvector distance operator and the operator-class suffix its index takes, in one
48
+ * place so a dialect cannot end up with the operator but not the opclass. The key set is the single
49
+ * source of truth for which metrics the dialect supports at all: CockroachDB narrows it to three.
50
+ */
51
+ vectorMetrics = new Map([
52
+ ['cosine', { op: '<=>', opsSuffix: 'cosine' }],
53
+ ['l2', { op: '<->', opsSuffix: 'l2' }],
54
+ ['inner', { op: '<#>', opsSuffix: 'ip' }],
55
+ ['l1', { op: '<+>', opsSuffix: 'l1' }],
51
56
  ]);
52
57
  normalizeValue(value) {
53
58
  if (value != null && typeof value === 'object' && Array.isArray(value)) {
@@ -55,13 +60,61 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
55
60
  }
56
61
  return super.normalizeValue(value);
57
62
  }
63
+ /** pgvector's own index types; CockroachDB's native one widens this. */
64
+ isVectorIndex(index) {
65
+ return index.type === 'hnsw' || index.type === 'ivfflat';
66
+ }
67
+ indexAccessMethod(index) {
68
+ return index.type ? ` USING ${index.type}` : '';
69
+ }
70
+ indexFeatures = new Set([
71
+ 'expression',
72
+ 'nullsOrder',
73
+ 'opsClass',
74
+ 'include',
75
+ ]);
76
+ /**
77
+ * A vector index's operator class is named `{type}_{metric}_ops`: an index on a `halfvec` column
78
+ * needs `halfvec_cosine_ops`, and `vector_cosine_ops` there is rejected outright. An unsupported
79
+ * distance throws rather than being omitted, since a bare `USING hnsw ("embedding")` would build
80
+ * with the dialect's default metric instead of the one requested, with nothing signalling it.
81
+ * Everything else takes the operator class the entry declares, e.g. `jsonb_path_ops` for GIN.
82
+ */
83
+ indexColumnOpsClass(entry, index) {
84
+ if (!this.isVectorIndex(index) || !index.distance) {
85
+ return entry.opsClass ? ` ${entry.opsClass}` : '';
86
+ }
87
+ const metric = this.vectorMetrics.get(index.distance);
88
+ if (!metric) {
89
+ throw new TypeError(`${this.dialectName} does not support vector distance metric: ${index.distance} (index "${index.name}")`);
90
+ }
91
+ const vectorType = this.supportedVectorType(index.vectorType ?? 'vector');
92
+ const opsClass = `${vectorType}_${metric.opsSuffix}_ops`;
93
+ // IVFFlat has neither a sparsevec nor an L1 operator class; HNSW has all of them (pgvector 0.8.2).
94
+ if (index.type === 'ivfflat' && (vectorType === 'sparsevec' || index.distance === 'l1')) {
95
+ throw new TypeError(`ivfflat has no ${opsClass} operator class (index "${index.name}"); use hnsw`);
96
+ }
97
+ return ` ${opsClass}`;
98
+ }
99
+ indexInclude(index) {
100
+ return index.include?.length ? ` INCLUDE (${index.include.map((column) => this.escapeId(column)).join(', ')})` : '';
101
+ }
102
+ indexTuning(index) {
103
+ if (!this.isVectorIndex(index)) {
104
+ return '';
105
+ }
106
+ const params = [];
107
+ if (index.m !== undefined)
108
+ params.push(`m = ${index.m}`);
109
+ if (index.efConstruction !== undefined)
110
+ params.push(`ef_construction = ${index.efConstruction}`);
111
+ if (index.lists !== undefined)
112
+ params.push(`lists = ${index.lists}`);
113
+ return params.length > 0 ? ` WITH (${params.join(', ')})` : '';
114
+ }
58
115
  placeholder(index) {
59
116
  return `$${index}`;
60
117
  }
61
- insert(ctx, entity, payload, opts) {
62
- super.insert(ctx, entity, payload, opts);
63
- ctx.append(' ' + this.returningId(entity));
64
- }
65
118
  upsert(ctx, entity, conflictPaths, payload) {
66
119
  this.buildUpsertOnConflict(ctx, entity, conflictPaths, payload);
67
120
  }
@@ -77,33 +130,23 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
77
130
  const update = this.getUpsertUpdateAssignments(ctx, meta, conflictPaths, payload, (name) => `EXCLUDED.${name}`);
78
131
  const keysStr = this.getUpsertConflictPathsStr(meta, conflictPaths);
79
132
  const onConflict = update ? `DO UPDATE SET ${update}` : 'DO NOTHING';
80
- super.insert(ctx, entity, payload);
133
+ this.appendInsertValues(ctx, entity, payload);
81
134
  ctx.append(` ON CONFLICT (${keysStr}) ${onConflict} ${this.returningId(entity)}${extraReturning}`);
82
135
  }
83
136
  /**
84
- * Full-text search: `to_tsvector(...) @@ websearch_to_tsquery(...)`. `websearch_to_tsquery` takes
85
- * free-form user input (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike
86
- * `to_tsquery`, which rejects anything unparseable - including a plain two-word search.
137
+ * `to_tsvector(...) @@ websearch_to_tsquery(...)`. `websearch_to_tsquery` takes free-form user input
138
+ * (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `to_tsquery`, which
139
+ * rejects anything unparseable - including a plain two-word search.
87
140
  */
88
- compare(ctx, entity, key, val, opts = {}) {
89
- if (key === '$text') {
90
- const meta = getMeta(entity);
91
- const search = val;
92
- const fields = (search.$fields ?? [])
93
- .map((fKey) => {
94
- const field = meta.fields[fKey];
95
- const columnName = this.resolveColumnName(fKey, field);
96
- return this.escapeId(columnName);
97
- })
98
- .join(` || ' ' || `);
99
- // The config is bound once and its numbered placeholder reused by both calls.
100
- const config = search.$config ? `${this.addValue(ctx.values, search.$config)}::regconfig, ` : '';
101
- ctx.append(`to_tsvector(${config}${fields}) @@ websearch_to_tsquery(${config}`);
102
- ctx.addValue(search.$value);
103
- ctx.append(')');
104
- return;
105
- }
106
- super.compare(ctx, entity, key, val, opts);
141
+ appendTextSearch(ctx, _entity, meta, search) {
142
+ const fields = (search.$fields ?? [])
143
+ .map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])))
144
+ .join(` || ' ' || `);
145
+ // The config is bound once and its numbered placeholder reused by both calls.
146
+ const config = search.$config ? `${this.addValue(ctx.values, search.$config)}::regconfig, ` : '';
147
+ ctx.append(`to_tsvector(${config}${fields}) @@ websearch_to_tsquery(${config}`);
148
+ ctx.addValue(search.$value);
149
+ ctx.append(')');
107
150
  }
108
151
  jsonAll(ctx, jsonField, value) {
109
152
  return `${jsonField} @> ${this.jsonVal(ctx, value)}`;
@@ -146,9 +189,14 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
146
189
  appendJsonValue(ctx, value, type) {
147
190
  ctx.append(this.jsonVal(ctx, value, type));
148
191
  }
149
- appendVectorValue(ctx, value) {
150
- ctx.addValue(`[${value.join(',')}]`);
151
- ctx.append('::vector');
192
+ /**
193
+ * pgvector needs the cast to pick the right type, and `sparsevec` needs its own literal: the dense
194
+ * `[1,0,2]` every other type takes is rejected as "invalid input syntax for type sparsevec".
195
+ */
196
+ appendVectorValue(ctx, value, field) {
197
+ const vectorType = this.supportedVectorType(resolveVectorCast(field));
198
+ ctx.addValue(vectorType === 'sparsevec' ? toSparsevecLiteral(value) : `[${value.join(',')}]`);
199
+ ctx.append(`::${vectorType}`);
152
200
  }
153
201
  /**
154
202
  * `create_if_missing => false` keeps an absent key (and a NULL column) untouched; `WITH
@@ -189,29 +237,15 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
189
237
  const ph = this.addValue(ctx.values, json);
190
238
  return this.features.explicitJsonCast ? `(${ph}::text)::${type}` : `${ph}::${type}`;
191
239
  }
192
- /**
193
- * pgvector distance operators. Not every dialect supports every metric - see
194
- * {@link CockroachDialect.vectorOpsClass} for which ones each dialect actually has; the
195
- * `vectorOpsClass` key set (checked in `appendVectorSort` below via `vectorOpsClass.get`)
196
- * is the single source of truth for that, not this map.
197
- */
198
- static VECTOR_OPS = {
199
- cosine: '<=>',
200
- l2: '<->',
201
- inner: '<#>',
202
- l1: '<+>',
203
- hamming: '<~>',
204
- };
205
240
  /** Emit a pgvector-style distance expression: `"col" <op> $N::<vectorType>`. */
206
241
  appendVectorSort(ctx, meta, key, search) {
207
- const { colName, distance, vectorCast } = this.resolveVectorSortParams(meta, key, search);
208
- if (!this.vectorOpsClass.get(distance)) {
242
+ const { colName, distance, field } = this.resolveVectorSortParams(meta, key, search);
243
+ const metric = this.vectorMetrics.get(distance);
244
+ if (!metric) {
209
245
  throw new TypeError(`${this.dialectName} does not support vector distance metric: ${distance}`);
210
246
  }
211
- const op = PgLikeSqlDialect.VECTOR_OPS[distance];
212
- ctx.append(`${this.escapeId(colName)} ${op} `);
213
- ctx.addValue(`[${search.$vector.join(',')}]`);
214
- ctx.append(`::${vectorCast}`);
247
+ ctx.append(`${this.escapeId(colName)} ${metric.op} `);
248
+ this.appendVectorValue(ctx, search.$vector, field);
215
249
  }
216
250
  }
217
251
  /**
@@ -2,10 +2,31 @@
2
2
  * Kept out of `schema/canonicalType.ts`: importing one function from that migration/codegen module
3
3
  * pulled all ~18 KB of its type-mapping tables into every consumer's bundle.
4
4
  */
5
+ import type { DialectName } from '../type/querier.js';
5
6
  /** Vector cast types supported by pgvector. */
6
7
  export type VectorCast = 'vector' | 'halfvec' | 'sparsevec';
8
+ /**
9
+ * The dialects that have more than one vector column type. pgvector is the only one: `halfvec` and
10
+ * `sparsevec` exist nowhere else (`HALFVEC(3)` is a syntax error on CockroachDB 26.2 and MariaDB 12.3,
11
+ * not merely unsupported at runtime), so a field declaring one is stored - and therefore cast - as
12
+ * plain `vector`. Both halves of that used to be stated separately, in this dialect layer and in the
13
+ * migration type maps.
14
+ */
15
+ export declare const MULTI_VECTOR_TYPE_DIALECTS: ReadonlySet<DialectName>;
16
+ /**
17
+ * Whether a declared field type is a vector of any width. Every dialect that treats vectors specially
18
+ * has to answer this for all three, not just `vector`: matching that one alone left `halfvec` and
19
+ * `sparsevec` fields binding as plain arrays on insert and reading back raw.
20
+ */
21
+ export declare function isVectorFieldType(type: unknown): boolean;
7
22
  /** Resolves the effective cast from field options, `columnType` taking priority over `type`. */
8
23
  export declare function resolveVectorCast(field: {
9
24
  type?: unknown;
10
25
  columnType?: unknown;
11
26
  } | undefined): VectorCast;
27
+ /**
28
+ * pgvector's `sparsevec` literal: 1-based `index:value` pairs of the non-zero elements, then the
29
+ * dimension count (`{1:1,3:2}/3`). A dense `[1,0,2]` is rejected outright by that type, so an entity
30
+ * declaring `type: 'sparsevec'` still hands UQL the dense array its field type promises.
31
+ */
32
+ export declare function toSparsevecLiteral(values: readonly unknown[]): string;
@@ -2,6 +2,22 @@
2
2
  * Kept out of `schema/canonicalType.ts`: importing one function from that migration/codegen module
3
3
  * pulled all ~18 KB of its type-mapping tables into every consumer's bundle.
4
4
  */
5
+ /**
6
+ * The dialects that have more than one vector column type. pgvector is the only one: `halfvec` and
7
+ * `sparsevec` exist nowhere else (`HALFVEC(3)` is a syntax error on CockroachDB 26.2 and MariaDB 12.3,
8
+ * not merely unsupported at runtime), so a field declaring one is stored - and therefore cast - as
9
+ * plain `vector`. Both halves of that used to be stated separately, in this dialect layer and in the
10
+ * migration type maps.
11
+ */
12
+ export const MULTI_VECTOR_TYPE_DIALECTS = new Set(['postgres']);
13
+ /**
14
+ * Whether a declared field type is a vector of any width. Every dialect that treats vectors specially
15
+ * has to answer this for all three, not just `vector`: matching that one alone left `halfvec` and
16
+ * `sparsevec` fields binding as plain arrays on insert and reading back raw.
17
+ */
18
+ export function isVectorFieldType(type) {
19
+ return type === 'vector' || type === 'halfvec' || type === 'sparsevec';
20
+ }
5
21
  /** Resolves the effective cast from field options, `columnType` taking priority over `type`. */
6
22
  export function resolveVectorCast(field) {
7
23
  const raw = field?.columnType ?? field?.type;
@@ -11,3 +27,15 @@ export function resolveVectorCast(field) {
11
27
  return 'sparsevec';
12
28
  return 'vector';
13
29
  }
30
+ /**
31
+ * pgvector's `sparsevec` literal: 1-based `index:value` pairs of the non-zero elements, then the
32
+ * dimension count (`{1:1,3:2}/3`). A dense `[1,0,2]` is rejected outright by that type, so an entity
33
+ * declaring `type: 'sparsevec'` still hands UQL the dense array its field type promises.
34
+ */
35
+ export function toSparsevecLiteral(values) {
36
+ const pairs = values
37
+ .map((value, index) => `${index + 1}:${value}`)
38
+ .filter((_, index) => Number(values[index]) !== 0)
39
+ .join(',');
40
+ return `{${pairs}}/${values.length}`;
41
+ }
@@ -14,8 +14,6 @@ import { type VectorCast } from './vectorCast.js';
14
14
  * {@link appendVectorSort} or filling {@link vectorDistanceFns}.
15
15
  */
16
16
  export declare abstract class VectorSqlDialect extends AbstractDialect {
17
- /** Vector index operator classes, keyed by distance metric. Partial: not every dialect supports every metric. */
18
- readonly vectorOpsClass: ReadonlyMap<VectorDistance, string> | undefined;
19
17
  readonly vectorExtension: string | undefined;
20
18
  /**
21
19
  * Mapping of UQL vector distance metrics to native SQL functions.
@@ -33,8 +31,17 @@ export declare abstract class VectorSqlDialect extends AbstractDialect {
33
31
  colName: string;
34
32
  distance: VectorDistance;
35
33
  field: FieldOptions | undefined;
36
- vectorCast: VectorCast;
37
34
  };
35
+ /**
36
+ * Binds a vector, both as a persisted value and as the query vector of a distance expression, so a
37
+ * dialect needing a conversion around it (`$1::vector`, `VEC_FromText(?)`) declares it once.
38
+ */
39
+ protected appendVectorValue(ctx: QueryContext, value: readonly unknown[], _field?: FieldOptions): void;
40
+ /**
41
+ * The vector type this dialect actually has for a declared one, so the cast follows the column
42
+ * rather than naming a type the engine does not define. See {@link MULTI_VECTOR_TYPE_DIALECTS}.
43
+ */
44
+ protected supportedVectorType(cast: VectorCast): VectorCast;
38
45
  /**
39
46
  * Append a vector similarity function call: `fn(col, ?)`.
40
47
  * Used by dialects that express vector distance via SQL functions (SQLite, MariaDB).