uql-orm 0.70.0 → 0.71.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.
- package/README.md +2 -0
- package/dist/cockroachdb/cockroachDialect.d.ts +6 -0
- package/dist/cockroachdb/cockroachDialect.js +8 -0
- package/dist/d1/d1SqliteDialect.d.ts +3 -0
- package/dist/d1/d1SqliteDialect.js +3 -1
- package/dist/dialect/abstractSqlDialect.d.ts +5 -0
- package/dist/dialect/abstractSqlDialect.js +12 -5
- package/dist/dialect/hydrateColumn.d.ts +3 -2
- package/dist/dialect/hydrateColumn.js +10 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +2 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +17 -5
- package/dist/dialect/pgLikeSqlDialect.js +33 -15
- package/dist/dialect/queryJoins.d.ts +14 -3
- package/dist/dialect/queryJoins.js +31 -11
- package/dist/dialect/vectorCast.d.ts +2 -0
- package/dist/dialect/vectorCast.js +7 -0
- package/dist/dialect/vectorSqlDialect.d.ts +7 -3
- package/dist/dialect/vectorSqlDialect.js +15 -10
- package/dist/libsql/libsqlDialect.d.ts +1 -1
- package/dist/libsql/libsqlDialect.js +3 -3
- package/dist/maria/mariaDialect.d.ts +3 -9
- package/dist/maria/mariaDialect.js +4 -13
- package/dist/maria/mariadbQuerier.js +9 -3
- package/dist/migrate/ddl/index.d.ts +1 -0
- package/dist/migrate/ddl/index.js +4 -2
- package/dist/migrate/ddl/indexDdl.d.ts +2 -0
- package/dist/migrate/ddl/indexDdl.js +10 -2
- package/dist/migrate/ddl/pgIndexDdl.d.ts +3 -4
- package/dist/migrate/ddl/pgIndexDdl.js +11 -11
- package/dist/migrate/ddl/sqliteIndexDdl.d.ts +11 -0
- package/dist/migrate/ddl/sqliteIndexDdl.js +38 -0
- package/dist/migrate/generator/mongoCommand.d.ts +28 -0
- package/dist/migrate/generator/mongoCommand.js +8 -0
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +2 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +50 -6
- package/dist/migrate/introspection/mongoIntrospector.js +33 -7
- package/dist/migrate/introspection/postgresIntrospector.js +19 -0
- package/dist/migrate/introspection/sqliteIntrospector.d.ts +6 -0
- package/dist/migrate/introspection/sqliteIntrospector.js +29 -3
- package/dist/mongo/mongoDialect.d.ts +2 -0
- package/dist/mongo/mongoDialect.js +8 -4
- package/dist/mongo/mongodbQuerier.js +2 -2
- package/dist/mssql/mssqlDialect.js +1 -0
- package/dist/schema/canonicalType.js +10 -4
- package/dist/schema/indexDifferences.d.ts +5 -2
- package/dist/schema/indexDifferences.js +5 -0
- package/dist/schema/schemaASTBuilder.js +3 -2
- package/dist/sqlite/localSqliteQuerierPool.d.ts +10 -11
- package/dist/sqlite/localSqliteQuerierPool.js +8 -11
- package/dist/sqlite/nodeSqliteQuerierPool.d.ts +4 -2
- package/dist/sqlite/nodeSqliteQuerierPool.js +7 -4
- package/dist/sqlite/sqliteDialect.d.ts +9 -1
- package/dist/sqlite/sqliteDialect.js +42 -3
- package/dist/sqlite/sqliteQuerierPool.d.ts +6 -5
- package/dist/sqlite/sqliteQuerierPool.js +10 -7
- package/dist/turso/tursoLocalDialect.d.ts +1 -1
- package/dist/turso/tursoLocalDialect.js +1 -1
- package/dist/turso/tursoLocalQuerierPool.d.ts +4 -5
- package/dist/turso/tursoLocalQuerierPool.js +3 -10
- package/dist/type/dialect.d.ts +5 -0
- package/dist/type/entity.d.ts +14 -3
- package/dist/type/migration.d.ts +7 -9
- package/dist/type/queryAggregate.d.ts +4 -8
- package/dist/type/vector.d.ts +17 -0
- package/dist/type/vector.js +6 -0
- package/dist/util/ddlExpression.util.d.ts +2 -0
- package/dist/util/ddlExpression.util.js +5 -0
- package/dist/util/dialect.util.d.ts +11 -1
- package/dist/util/dialect.util.js +21 -1
- package/package.json +5 -3
- package/skills/uql-orm/SKILL.md +142 -0
package/README.md
CHANGED
|
@@ -65,6 +65,8 @@ The compiler catches each of those, with no codegen: the entity classes are the
|
|
|
65
65
|
- [Entities](https://uql-orm.dev/entities/basic) - decorators, relations, hooks, or the decorator-free [imperative API](https://uql-orm.dev/entities/imperative)
|
|
66
66
|
- [Switching to UQL](https://uql-orm.dev/switching-to-uql) - coming from Prisma, Drizzle, TypeORM, or MikroORM
|
|
67
67
|
|
|
68
|
+
Using a coding agent? The package ships a [skill](skills/uql-orm/SKILL.md) for it (`npx @tanstack/intent install`, or `npx skills add rogerpadilla/uql`), and every docs page is Markdown, indexed at [uql-orm.dev/llms.txt](https://uql-orm.dev/llms.txt).
|
|
69
|
+
|
|
68
70
|
Release notes live in [CHANGELOG.md](https://github.com/rogerpadilla/uql/blob/main/CHANGELOG.md).
|
|
69
71
|
|
|
70
72
|
---
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PgLikeSqlDialect } from '../dialect/pgLikeSqlDialect.js';
|
|
2
|
+
import type { IndexType } from '../schema/types.js';
|
|
2
3
|
import type { QueryContext, SqlDialectFeatures, Type } from '../type/index.js';
|
|
3
4
|
/** CockroachDB: the Postgres wire and SQL, without `xmax` (so no upsert `created`) and with native vectors. */
|
|
4
5
|
export declare class CockroachDialect extends PgLikeSqlDialect {
|
|
@@ -9,6 +10,11 @@ export declare class CockroachDialect extends PgLikeSqlDialect {
|
|
|
9
10
|
* Re-check that issue before adding it.
|
|
10
11
|
*/
|
|
11
12
|
readonly vectorMetrics: Map<import("../type/vector.js").VectorDistance, import("../type/vector.js").VectorMetric>;
|
|
13
|
+
/** Neither `regconfig` nor `WEBSEARCH_TO_TSQUERY` exist here (v26.3): the config goes as text, the search as plain words. */
|
|
14
|
+
protected readonly textConfigCast = "";
|
|
15
|
+
protected readonly textQueryFn = "PLAINTO_TSQUERY";
|
|
16
|
+
/** Its own beam for either type that builds its vector index; it refuses pgvector's `hnsw.ef_search`. */
|
|
17
|
+
protected readonly annSettings: ReadonlyMap<IndexType, string>;
|
|
12
18
|
/** An upsert batch mixing an update and an insert returns the update first (verified on v26.2). */
|
|
13
19
|
readonly features: SqlDialectFeatures;
|
|
14
20
|
/**
|
|
@@ -10,6 +10,14 @@ export class CockroachDialect extends PgLikeSqlDialect {
|
|
|
10
10
|
* Re-check that issue before adding it.
|
|
11
11
|
*/
|
|
12
12
|
vectorMetrics = new Map([...PG_VECTOR_METRICS].filter(([metric]) => metric !== 'l1'));
|
|
13
|
+
/** Neither `regconfig` nor `WEBSEARCH_TO_TSQUERY` exist here (v26.3): the config goes as text, the search as plain words. */
|
|
14
|
+
textConfigCast = '';
|
|
15
|
+
textQueryFn = 'PLAINTO_TSQUERY';
|
|
16
|
+
/** Its own beam for either type that builds its vector index; it refuses pgvector's `hnsw.ef_search`. */
|
|
17
|
+
annSettings = new Map([
|
|
18
|
+
['hnsw', 'vector_search_beam_size'],
|
|
19
|
+
['vector', 'vector_search_beam_size'],
|
|
20
|
+
]);
|
|
13
21
|
/** An upsert batch mixing an update and an insert returns the update first (verified on v26.2). */
|
|
14
22
|
features = { ...PG_FEATURES, orderedUpsertReturning: false };
|
|
15
23
|
/**
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { SqliteDialect } from '../sqlite/sqliteDialect.js';
|
|
2
|
+
import type { SqlDialectFeatures } from '../type/index.js';
|
|
2
3
|
/**
|
|
3
4
|
* SQLite Dialect specialization for Cloudflare D1.
|
|
4
5
|
*
|
|
5
6
|
* @remarks Distinct type for `D1QuerierPool` and a hook for D1-specific SQL differences.
|
|
6
7
|
*/
|
|
7
8
|
export declare class D1SqliteDialect extends SqliteDialect {
|
|
9
|
+
/** A vector stays text: D1 answers a BLOB as an array of its byte values, which reads like a vector. */
|
|
10
|
+
readonly features: SqlDialectFeatures;
|
|
8
11
|
readonly maxBindValues: number;
|
|
9
12
|
readonly maxFunctionArgs: number;
|
|
10
13
|
/**
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { SqliteDialect } from '../sqlite/sqliteDialect.js';
|
|
1
|
+
import { SQLITE_FEATURES, SqliteDialect } from '../sqlite/sqliteDialect.js';
|
|
2
2
|
/**
|
|
3
3
|
* SQLite Dialect specialization for Cloudflare D1.
|
|
4
4
|
*
|
|
5
5
|
* @remarks Distinct type for `D1QuerierPool` and a hook for D1-specific SQL differences.
|
|
6
6
|
*/
|
|
7
7
|
export class D1SqliteDialect extends SqliteDialect {
|
|
8
|
+
/** A vector stays text: D1 answers a BLOB as an array of its byte values, which reads like a vector. */
|
|
9
|
+
features = { ...SQLITE_FEATURES, vectorBytes: false };
|
|
8
10
|
// Cloudflare D1 caps bound parameters at 100 per query.
|
|
9
11
|
maxBindValues = 100;
|
|
10
12
|
// And a function call at 32 arguments.
|
|
@@ -446,6 +446,11 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
446
446
|
*/
|
|
447
447
|
private readOptions;
|
|
448
448
|
insert<E>(ctx: QueryContext, entity: Type<E>, payload: E | E[], opts?: QueryOptions): void;
|
|
449
|
+
/**
|
|
450
|
+
* What a text search matches and a fulltext index covers, which have to agree for the index to serve the
|
|
451
|
+
* search: the columns themselves, where the engine indexes them as they are (MySQL's `MATCH (a, b)`).
|
|
452
|
+
*/
|
|
453
|
+
textSearchTarget(columns: readonly string[], _config?: string): string;
|
|
449
454
|
/** Where an insert's id clause goes: `RETURNING` at the end, or SQL Server's `OUTPUT` before `VALUES`. */
|
|
450
455
|
readonly returningPosition: 'suffix' | 'after-target';
|
|
451
456
|
/**
|
|
@@ -150,7 +150,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
150
150
|
if (opts.prefix !== prefix) {
|
|
151
151
|
opts = { ...opts, prefix };
|
|
152
152
|
}
|
|
153
|
-
this.where(ctx, entity, q
|
|
153
|
+
this.where(ctx, entity, this.rankedWhere(meta, q, prefix), opts);
|
|
154
154
|
const sorted = order
|
|
155
155
|
? this.orderCarried(ctx, q, order)
|
|
156
156
|
: this.sort(ctx, entity, q.$sort, { prefix, joins, distinct: q.$distinct });
|
|
@@ -963,7 +963,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
963
963
|
throw new TypeError('aggregate requires at least one $group column or $select function');
|
|
964
964
|
}
|
|
965
965
|
const table = this.tableRef(meta, this.readOptions(ctx, meta).alias);
|
|
966
|
-
const joins = resolveGroupJoins(meta, q
|
|
966
|
+
const { joins, where } = resolveGroupJoins(meta, q, (path) => ctx.claimAlias(path));
|
|
967
967
|
const prefix = joins.size ? table.alias : undefined;
|
|
968
968
|
const reads = entries.map((entry) => ({ entry, value: this.aggregateValue(ctx, entity, entry, joins, prefix) }));
|
|
969
969
|
// Only bare columns are read inline: SQL Server refuses a subquery inside an aggregate or a
|
|
@@ -988,7 +988,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
988
988
|
const columns = reads.flatMap(({ entry, value }) => (value.sql === '*' ? [] : [named(value.sql, entry.alias)]));
|
|
989
989
|
ctx.append(`SELECT ${selectParts.join(', ')} FROM ${derived ? `(SELECT ${columns.join(', ')} FROM ` : ''}${table.ref}`);
|
|
990
990
|
this.selectRelationJoins(ctx, meta, table.alias, joins);
|
|
991
|
-
this.where(ctx, entity,
|
|
991
|
+
this.where(ctx, entity, where, { ...opts, prefix });
|
|
992
992
|
if (derived) {
|
|
993
993
|
ctx.append(`) ${this.escapeId(ROWS_ALIAS, true)}`);
|
|
994
994
|
}
|
|
@@ -1127,6 +1127,13 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1127
1127
|
ctx.append(` ${returning}`);
|
|
1128
1128
|
}
|
|
1129
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* What a text search matches and a fulltext index covers, which have to agree for the index to serve the
|
|
1132
|
+
* search: the columns themselves, where the engine indexes them as they are (MySQL's `MATCH (a, b)`).
|
|
1133
|
+
*/
|
|
1134
|
+
textSearchTarget(columns, _config) {
|
|
1135
|
+
return columns.join(', ');
|
|
1136
|
+
}
|
|
1130
1137
|
/** Where an insert's id clause goes: `RETURNING` at the end, or SQL Server's `OUTPUT` before `VALUES`. */
|
|
1131
1138
|
returningPosition = 'suffix';
|
|
1132
1139
|
/**
|
|
@@ -1340,7 +1347,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1340
1347
|
/** The same for an aggregate's row, per query: each alias decodes by {@link aggregateKind}. */
|
|
1341
1348
|
hydratableAggregates(entity, q) {
|
|
1342
1349
|
const meta = getMeta(entity);
|
|
1343
|
-
const joins = resolveGroupJoins(meta, q
|
|
1350
|
+
const { joins } = resolveGroupJoins(meta, q);
|
|
1344
1351
|
const decoded = [];
|
|
1345
1352
|
for (const entry of parseGroupMap(q.$group, q.$select)) {
|
|
1346
1353
|
const kind = entry.kind === 'fn'
|
|
@@ -1387,7 +1394,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1387
1394
|
case 'json':
|
|
1388
1395
|
return 'json';
|
|
1389
1396
|
case 'vector':
|
|
1390
|
-
return this.supportedVectorType(resolveVectorCast(field));
|
|
1397
|
+
return this.features.vectorBytes ? 'float32' : this.supportedVectorType(resolveVectorCast(field));
|
|
1391
1398
|
case 'boolean':
|
|
1392
1399
|
return 'boolean';
|
|
1393
1400
|
case 'numeric':
|
|
@@ -3,9 +3,10 @@ import { type VectorCast } from './vectorCast.js';
|
|
|
3
3
|
* How a stored column is decoded on read: the inverse of `AbstractSqlDialect.persistKind`. `json`
|
|
4
4
|
* parses; a {@link VectorCast} says which literal; `boolean` undoes an engine with no boolean type,
|
|
5
5
|
* `number` and `bigint` a driver that hands a wide integer or a decimal back as text, and `date` and
|
|
6
|
-
* `bytes` a row that crossed JSON inside its parent's statement, which spells both as text.
|
|
6
|
+
* `bytes` a row that crossed JSON inside its parent's statement, which spells both as text. `float32` is
|
|
7
|
+
* a vector bound as bytes (`DialectFeatures.vectorBytes`).
|
|
7
8
|
*/
|
|
8
|
-
export type HydrateKind = 'json' | 'boolean' | 'number' | 'bigint' | 'date' | 'bytes' | VectorCast;
|
|
9
|
+
export type HydrateKind = 'json' | 'boolean' | 'number' | 'bigint' | 'date' | 'bytes' | 'float32' | VectorCast;
|
|
9
10
|
/**
|
|
10
11
|
* Decodes one non-null cell. A no-op where the driver already decoded it, since that varies per driver,
|
|
11
12
|
* and untouched where it does not match its column's format.
|
|
@@ -20,6 +20,14 @@ function vectorDecoder(cast) {
|
|
|
20
20
|
? decodeFloat32s(hexBytes(text.slice(BYTES_PREFIX.length)))
|
|
21
21
|
: (parseVectorLiteral(text, cast) ?? value));
|
|
22
22
|
}
|
|
23
|
+
const denseVector = vectorDecoder('vector');
|
|
24
|
+
/** A vector bound as bytes: packed float32s as a driver returns them, else hex or text, as JSON or an older row carries it. */
|
|
25
|
+
const float32Decoder = (value) => {
|
|
26
|
+
if (value instanceof ArrayBuffer) {
|
|
27
|
+
return decodeFloat32s(new Uint8Array(value));
|
|
28
|
+
}
|
|
29
|
+
return value instanceof Uint8Array ? decodeFloat32s(value) : denseVector(value);
|
|
30
|
+
};
|
|
23
31
|
const DECODERS = {
|
|
24
32
|
// 0/1 from SQLite's INTEGER or MySQL's TINYINT(1). Already a boolean on Postgres.
|
|
25
33
|
boolean: (value) => (typeof value === 'boolean' ? value : Boolean(value)),
|
|
@@ -48,7 +56,8 @@ const DECODERS = {
|
|
|
48
56
|
return value;
|
|
49
57
|
}
|
|
50
58
|
}),
|
|
51
|
-
|
|
59
|
+
float32: float32Decoder,
|
|
60
|
+
vector: denseVector,
|
|
52
61
|
halfvec: vectorDecoder('halfvec'),
|
|
53
62
|
sparsevec: vectorDecoder('sparsevec'),
|
|
54
63
|
};
|
|
@@ -23,6 +23,7 @@ export const MYSQL_FEATURES = {
|
|
|
23
23
|
commentSyntax: 'inline',
|
|
24
24
|
vectorIndexRequiresNotNull: false,
|
|
25
25
|
vectorSupportsLength: false,
|
|
26
|
+
vectorBytes: false,
|
|
26
27
|
supportsTimestamptz: false,
|
|
27
28
|
stringSizing: 'varchar',
|
|
28
29
|
supportsUnsigned: true,
|
|
@@ -178,7 +179,7 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
178
179
|
*/
|
|
179
180
|
appendTextSearch(ctx, _entity, meta, search) {
|
|
180
181
|
const columns = textSearchFields(meta, search).map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])));
|
|
181
|
-
ctx.append(`MATCH(${
|
|
182
|
+
ctx.append(`MATCH(${this.textSearchTarget(columns)}) AGAINST(`);
|
|
182
183
|
ctx.addValue(search.$value);
|
|
183
184
|
ctx.append(')');
|
|
184
185
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IndexType } from '../schema/types.js';
|
|
1
2
|
import { type DriverCapabilities, type EntityMeta, type FieldOptions, type JsonColumnType, type Query, type QueryContext, type QueryTextSearchOptions, type SqlDialectFeatures, type Type, type VectorDistance, type VectorMetric } from '../type/index.js';
|
|
2
3
|
import type { DialectOptions } from './abstractDialect.js';
|
|
3
4
|
import { AbstractSqlDialect, type RelationRows } from './abstractSqlDialect.js';
|
|
@@ -45,11 +46,11 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
45
46
|
readonly maxBindValues: number;
|
|
46
47
|
readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
47
48
|
/**
|
|
48
|
-
* The
|
|
49
|
+
* The setting each vector index type reads for "how much of the index to explore". They are not the
|
|
49
50
|
* same quantity - `ef_search` is a candidate-list size, `probes` a count of lists - which is why
|
|
50
51
|
* `$candidates` is documented in the index's own units rather than as a portable number.
|
|
51
52
|
*/
|
|
52
|
-
|
|
53
|
+
protected readonly annSettings: ReadonlyMap<IndexType, string>;
|
|
53
54
|
/**
|
|
54
55
|
* `SET LOCAL hnsw.ef_search = N`, plus `hnsw.iterative_scan = strict_order` where the query also filters
|
|
55
56
|
* by distance, which would otherwise drop rows the candidate list missed.
|
|
@@ -58,11 +59,22 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
58
59
|
normalizeValue(value: unknown): unknown;
|
|
59
60
|
placeholder(index: number): string;
|
|
60
61
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
62
|
+
* `<document> @@ WEBSEARCH_TO_TSQUERY(...)`, under the `$config` asked for, else that of the fulltext
|
|
63
|
+
* index over these fields, which the planner serves it from. `WEBSEARCH_TO_TSQUERY` takes free-form
|
|
64
|
+
* input (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `TO_TSQUERY`.
|
|
64
65
|
*/
|
|
65
66
|
protected appendTextSearch<E>(ctx: QueryContext, _entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
|
|
67
|
+
/**
|
|
68
|
+
* The document, `TO_TSVECTOR('english'::regconfig, COALESCE("a", '') || ' ' || COALESCE("b", ''))`, a
|
|
69
|
+
* `NULL` column read as empty rather than emptying it all. The config is a literal: an index is built
|
|
70
|
+
* over one, and a bound one reaches it only where the driver leaves the parameter untyped.
|
|
71
|
+
*/
|
|
72
|
+
textSearchTarget(columns: readonly string[], config?: string): string;
|
|
73
|
+
/** A config as the text-search functions' first argument, or nothing, for the server's default. */
|
|
74
|
+
private textConfigArg;
|
|
75
|
+
/** How a config literal is typed, and the function reading a search's text: CockroachDB lacks both of these. */
|
|
76
|
+
protected readonly textConfigCast: string;
|
|
77
|
+
protected readonly textQueryFn: string;
|
|
66
78
|
protected jsonContains(ctx: QueryContext, slot: JsonSlot, values: readonly unknown[]): string;
|
|
67
79
|
protected jsonLength(slot: JsonSlot): string;
|
|
68
80
|
/** Each element stays `jsonb`, so it is read as any path is: `->` for the value, `->>` for its text. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { QueryRaw, } from '../type/index.js';
|
|
2
|
-
import { hasVectorNear, textSearchFields } from '../util/dialect.util.js';
|
|
2
|
+
import { fulltextConfig, fulltextIndexOver, hasVectorNear, textSearchFields } from '../util/dialect.util.js';
|
|
3
3
|
import { escapeSingleQuotes } from '../util/sqlLiteral.js';
|
|
4
4
|
import { AbstractSqlDialect } from './abstractSqlDialect.js';
|
|
5
5
|
import { JSON_PULL_ALIAS, RELATION_ROW_ALIAS } from './aliases.js';
|
|
@@ -13,6 +13,8 @@ export const PG_VECTOR_METRICS = new Map([
|
|
|
13
13
|
['inner', { op: '<#>', index: 'ip' }],
|
|
14
14
|
['l1', { op: '<+>', index: 'l1' }],
|
|
15
15
|
]);
|
|
16
|
+
/** pgvector's HNSW candidate list, the one setting an iterative scan goes with. */
|
|
17
|
+
const HNSW_EF_SEARCH = 'hnsw.ef_search';
|
|
16
18
|
/** What the Postgres-wire engines have. */
|
|
17
19
|
export const PG_FEATURES = {
|
|
18
20
|
ifNotExists: true,
|
|
@@ -25,6 +27,7 @@ export const PG_FEATURES = {
|
|
|
25
27
|
commentSyntax: 'statement',
|
|
26
28
|
vectorIndexRequiresNotNull: false,
|
|
27
29
|
vectorSupportsLength: true,
|
|
30
|
+
vectorBytes: false,
|
|
28
31
|
supportsTimestamptz: true,
|
|
29
32
|
stringSizing: 'bounded-text',
|
|
30
33
|
supportsUnsigned: false,
|
|
@@ -87,12 +90,12 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
87
90
|
maxBindValues = 65535;
|
|
88
91
|
vectorMetrics = PG_VECTOR_METRICS;
|
|
89
92
|
/**
|
|
90
|
-
* The
|
|
93
|
+
* The setting each vector index type reads for "how much of the index to explore". They are not the
|
|
91
94
|
* same quantity - `ef_search` is a candidate-list size, `probes` a count of lists - which is why
|
|
92
95
|
* `$candidates` is documented in the index's own units rather than as a portable number.
|
|
93
96
|
*/
|
|
94
|
-
|
|
95
|
-
['hnsw',
|
|
97
|
+
annSettings = new Map([
|
|
98
|
+
['hnsw', HNSW_EF_SEARCH],
|
|
96
99
|
['ivfflat', 'ivfflat.probes'],
|
|
97
100
|
]);
|
|
98
101
|
/**
|
|
@@ -101,12 +104,12 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
101
104
|
*/
|
|
102
105
|
vectorTuningStatements(meta, q) {
|
|
103
106
|
const indexType = this.tunedVectorIndex(meta, q)?.type;
|
|
104
|
-
const setting = indexType ?
|
|
107
|
+
const setting = indexType ? this.annSettings.get(indexType) : undefined;
|
|
105
108
|
if (!setting) {
|
|
106
109
|
return [];
|
|
107
110
|
}
|
|
108
111
|
const statements = [`SET LOCAL ${setting} = ${q.$candidates}`];
|
|
109
|
-
if (
|
|
112
|
+
if (setting === HNSW_EF_SEARCH && hasVectorNear(q.$where)) {
|
|
110
113
|
statements.push('SET LOCAL hnsw.iterative_scan = strict_order');
|
|
111
114
|
}
|
|
112
115
|
return statements;
|
|
@@ -121,20 +124,35 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
121
124
|
return `$${index}`;
|
|
122
125
|
}
|
|
123
126
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
+
* `<document> @@ WEBSEARCH_TO_TSQUERY(...)`, under the `$config` asked for, else that of the fulltext
|
|
128
|
+
* index over these fields, which the planner serves it from. `WEBSEARCH_TO_TSQUERY` takes free-form
|
|
129
|
+
* input (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `TO_TSQUERY`.
|
|
127
130
|
*/
|
|
128
131
|
appendTextSearch(ctx, _entity, meta, search) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
ctx.append(`TO_TSVECTOR(${config}${fields}) @@ WEBSEARCH_TO_TSQUERY(${config}`);
|
|
132
|
+
const keys = textSearchFields(meta, search);
|
|
133
|
+
const index = fulltextIndexOver(meta, keys);
|
|
134
|
+
const config = search.$config ?? (index && fulltextConfig(index));
|
|
135
|
+
const columns = keys.map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])));
|
|
136
|
+
ctx.append(`${this.textSearchTarget(columns, config)} @@ ${this.textQueryFn}(${this.textConfigArg(config)}`);
|
|
135
137
|
ctx.addValue(search.$value);
|
|
136
138
|
ctx.append(')');
|
|
137
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* The document, `TO_TSVECTOR('english'::regconfig, COALESCE("a", '') || ' ' || COALESCE("b", ''))`, a
|
|
142
|
+
* `NULL` column read as empty rather than emptying it all. The config is a literal: an index is built
|
|
143
|
+
* over one, and a bound one reaches it only where the driver leaves the parameter untyped.
|
|
144
|
+
*/
|
|
145
|
+
textSearchTarget(columns, config) {
|
|
146
|
+
const document = columns.map((column) => `COALESCE(${column}, '')`).join(` || ' ' || `);
|
|
147
|
+
return `TO_TSVECTOR(${this.textConfigArg(config)}${document})`;
|
|
148
|
+
}
|
|
149
|
+
/** A config as the text-search functions' first argument, or nothing, for the server's default. */
|
|
150
|
+
textConfigArg(config) {
|
|
151
|
+
return config === undefined ? '' : `${this.escape(config)}${this.textConfigCast}, `;
|
|
152
|
+
}
|
|
153
|
+
/** How a config literal is typed, and the function reading a search's text: CockroachDB lacks both of these. */
|
|
154
|
+
textConfigCast = '::regconfig';
|
|
155
|
+
textQueryFn = 'WEBSEARCH_TO_TSQUERY';
|
|
138
156
|
jsonContains(ctx, slot, values) {
|
|
139
157
|
return `${this.jsonValue(slot)} @> ${this.jsonVal(ctx, values)}`;
|
|
140
158
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EntityMeta, Query, QueryGroupMap, QuerySortMap, RelationMeta, RelationQuery, Type } from '../type/index.js';
|
|
1
|
+
import type { EntityMeta, Query, QueryGroupMap, QuerySortMap, QueryWhere, RelationMeta, RelationQuery, Type } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* One relation a statement joins, keyed by the alias its columns are addressed by (`tax`,
|
|
4
4
|
* `tax.category`). `projected` tells a `$populate` join, whose columns are selected, from one only
|
|
@@ -39,8 +39,19 @@ export type QuerySortOptions = {
|
|
|
39
39
|
* columns, the `ORDER BY` and the lock agree. `claimAlias` names each join's table, parents first.
|
|
40
40
|
*/
|
|
41
41
|
export declare function resolveQueryJoins<E>(meta: EntityMeta<E>, q: Query<E>, claimAlias?: (path: string) => string): QueryJoins;
|
|
42
|
-
/**
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* What an aggregate joins, and the `$where` left to it. Each to-one relation a `$group` path passes through
|
|
44
|
+
* is an `INNER` join, since a group of a path names a related row; a filter on one of them, keyed at the top
|
|
45
|
+
* of the `$where` where an `AND` joins it, moves into that join rather than reading its table again. Under a
|
|
46
|
+
* `$not` it could not: the join would drop the rows the negation keeps.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveGroupJoins<E>(meta: EntityMeta<E>, q: {
|
|
49
|
+
readonly $group?: QueryGroupMap<E>;
|
|
50
|
+
readonly $where?: QueryWhere<E>;
|
|
51
|
+
}, claimAlias?: (path: string) => string): {
|
|
52
|
+
readonly joins: QueryJoins;
|
|
53
|
+
readonly where: QueryWhere<E> | undefined;
|
|
54
|
+
};
|
|
44
55
|
/** The field a grouped `path` reads, and the join it reads it through: none for the entity's own. */
|
|
45
56
|
export declare function groupPathField(joins: QueryJoins, path: readonly string[]): {
|
|
46
57
|
readonly key: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getMeta, relationOf } from '../entity/index.js';
|
|
2
|
-
import { getKeys, getRelationRequestSummary, isRecord, isToManyRelation, parseRelationAtKey } from '../util/index.js';
|
|
2
|
+
import { getKeys, getRelationRequestSummary, isRecord, isToManyRelation, parseRelationAtKey, parseRelationSize, } from '../util/index.js';
|
|
3
3
|
export const NO_JOINS = new Map();
|
|
4
4
|
/**
|
|
5
5
|
* What the statement joins, from `$populate` and from a `$sort` by a to-one relation's field, so the
|
|
@@ -11,18 +11,35 @@ export function resolveQueryJoins(meta, q, claimAlias = (path) => path) {
|
|
|
11
11
|
}
|
|
12
12
|
const joins = new Map();
|
|
13
13
|
addPopulateJoins(joins, claimAlias, meta, q.$populate);
|
|
14
|
-
addPathJoins(joins, claimAlias, meta, q.$sort);
|
|
14
|
+
addPathJoins(joins, claimAlias, meta, q.$sort, false);
|
|
15
15
|
return joins;
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* What an aggregate joins, and the `$where` left to it. Each to-one relation a `$group` path passes through
|
|
19
|
+
* is an `INNER` join, since a group of a path names a related row; a filter on one of them, keyed at the top
|
|
20
|
+
* of the `$where` where an `AND` joins it, moves into that join rather than reading its table again. Under a
|
|
21
|
+
* `$not` it could not: the join would drop the rows the negation keeps.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveGroupJoins(meta, q, claimAlias = (path) => path) {
|
|
19
24
|
const joins = new Map();
|
|
20
|
-
for (const ref of Object.values(group ?? {})) {
|
|
25
|
+
for (const ref of Object.values(q.$group ?? {})) {
|
|
21
26
|
if (isRecord(ref)) {
|
|
22
|
-
addPathJoins(joins, claimAlias, meta, ref);
|
|
27
|
+
addPathJoins(joins, claimAlias, meta, ref, true);
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
|
-
|
|
30
|
+
if (!q.$where) {
|
|
31
|
+
return { joins, where: q.$where };
|
|
32
|
+
}
|
|
33
|
+
const where = { ...q.$where };
|
|
34
|
+
for (const key of getKeys(q.$where)) {
|
|
35
|
+
const join = joins.get(key);
|
|
36
|
+
const filter = q.$where[key];
|
|
37
|
+
if (join && isRecord(filter) && parseRelationSize(filter) === undefined) {
|
|
38
|
+
joins.set(key, { ...join, query: { $where: filter } });
|
|
39
|
+
delete where[key];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { joins, where };
|
|
26
43
|
}
|
|
27
44
|
/** The field a grouped `path` reads, and the join it reads it through: none for the entity's own. */
|
|
28
45
|
export function groupPathField(joins, path) {
|
|
@@ -93,8 +110,11 @@ function addPopulateJoins(joins, claimAlias, meta, populate, parent) {
|
|
|
93
110
|
addPopulateJoins(joins, claimAlias, join.meta, query.$populate, join);
|
|
94
111
|
}
|
|
95
112
|
}
|
|
96
|
-
/**
|
|
97
|
-
|
|
113
|
+
/**
|
|
114
|
+
* The to-one relations a nested map of fields passes through, a `$sort` or a `$group` path, as joins adding
|
|
115
|
+
* no columns: `required` where the path names a related row, as a group's does, and a sort's does not.
|
|
116
|
+
*/
|
|
117
|
+
function addPathJoins(joins, claimAlias, meta, map, required, parent) {
|
|
98
118
|
if (!map) {
|
|
99
119
|
return;
|
|
100
120
|
}
|
|
@@ -106,9 +126,9 @@ function addPathJoins(joins, claimAlias, meta, map, parent) {
|
|
|
106
126
|
if (!relation || isToManyRelation(relation) || !isSortMap(value)) {
|
|
107
127
|
continue;
|
|
108
128
|
}
|
|
109
|
-
const join = addJoin(joins, claimAlias, parent, key, relation, {},
|
|
129
|
+
const join = addJoin(joins, claimAlias, parent, key, relation, {}, required, false);
|
|
110
130
|
// `E` stated: inferred from the nested map, it lands on the nested relation's target.
|
|
111
|
-
addPathJoins(joins, claimAlias, join.meta, value, join);
|
|
131
|
+
addPathJoins(joins, claimAlias, join.meta, value, required, join);
|
|
112
132
|
}
|
|
113
133
|
}
|
|
114
134
|
/** The join a sort may address at `path` with the relation's own sort map, or why it may not; `unjoinable` is the dialect's remedy. */
|
|
@@ -20,5 +20,7 @@ export declare function toSparsevecLiteral(values: readonly unknown[]): string;
|
|
|
20
20
|
* as it was written, `{1:1,3:2}/3` or `[1,2,3]`; `undefined` where the text matches neither.
|
|
21
21
|
*/
|
|
22
22
|
export declare function parseVectorLiteral(raw: string, cast: VectorCast): number[] | undefined;
|
|
23
|
+
/** The packed little-endian float32s a blob vector column holds, and {@link decodeFloat32s} reads back. */
|
|
24
|
+
export declare function encodeFloat32s(values: readonly unknown[]): Uint8Array;
|
|
23
25
|
/** Packed little-endian float32s, each read as {@link shortestFloat32}. */
|
|
24
26
|
export declare function decodeFloat32s(bytes: Uint8Array): number[];
|
|
@@ -62,6 +62,13 @@ function parseDense(text) {
|
|
|
62
62
|
return undefined;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
|
+
/** The packed little-endian float32s a blob vector column holds, and {@link decodeFloat32s} reads back. */
|
|
66
|
+
export function encodeFloat32s(values) {
|
|
67
|
+
const bytes = new Uint8Array(values.length * 4);
|
|
68
|
+
const view = new DataView(bytes.buffer);
|
|
69
|
+
values.forEach((value, at) => view.setFloat32(at * 4, Number(value), true));
|
|
70
|
+
return bytes;
|
|
71
|
+
}
|
|
65
72
|
/** Packed little-endian float32s, each read as {@link shortestFloat32}. */
|
|
66
73
|
export function decodeFloat32s(bytes) {
|
|
67
74
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { EntityIndexMeta, EntityMeta, FieldOptions, Query, QueryContext, QueryVectorSearch, SqlDialectFeatures, VectorDistance, VectorMetric } from '../type/index.js';
|
|
1
|
+
import type { EntityIndexMeta, EntityMeta, FieldOptions, Query, QueryContext, QueryVectorSearch, QueryWhere, SqlDialectFeatures, VectorDistance, VectorMetric } from '../type/index.js';
|
|
2
2
|
import { AbstractDialect } from './abstractDialect.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type VectorCast } from './vectorCast.js';
|
|
4
4
|
/**
|
|
5
5
|
* Vector search for the SQL dialects: the distance a `$sort` ranks by and projects, and the ANN tuning.
|
|
6
6
|
* Each dialect lists its metrics in {@link vectorMetrics}, an operator or a function; empty means no search.
|
|
@@ -20,12 +20,16 @@ export declare abstract class VectorSqlDialect extends AbstractDialect {
|
|
|
20
20
|
* spelled into a `SET` rather than bound, and `/http` input is untyped.
|
|
21
21
|
*/
|
|
22
22
|
protected tunedVectorIndex<E>(meta: EntityMeta<E>, q: Query<E>): EntityIndexMeta | undefined;
|
|
23
|
+
/** The `$where` a read runs: the query's own, which an engine reading its vector index as a table narrows. */
|
|
24
|
+
protected rankedWhere<E>(_meta: EntityMeta<E>, q: Query<E>, _prefix: string | undefined): QueryWhere<E> | undefined;
|
|
23
25
|
/**
|
|
24
26
|
* Every distance metric this dialect has, and how a search and an index spell each. Empty means no
|
|
25
27
|
* vector search at all, which is what MySQL and D1 are. The key set is the single answer to "is this
|
|
26
28
|
* metric supported here", for a query and an index alike.
|
|
27
29
|
*/
|
|
28
30
|
readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
31
|
+
/** Whether this engine has a vector index: the one a metric's `index` names. */
|
|
32
|
+
hasVectorIndex(): boolean;
|
|
29
33
|
/** Quotes an identifier; supplied by the SQL dialect built on top of this layer. */
|
|
30
34
|
abstract escapeId(val: string | undefined, forbidQualified?: boolean, addDot?: boolean): string;
|
|
31
35
|
/**
|
|
@@ -39,7 +43,7 @@ export declare abstract class VectorSqlDialect extends AbstractDialect {
|
|
|
39
43
|
};
|
|
40
44
|
/**
|
|
41
45
|
* Binds a vector, both as a persisted value and as the query vector of a distance expression, so a
|
|
42
|
-
* dialect needing a conversion around it (`$1::vector`, `
|
|
46
|
+
* dialect needing a conversion around it (`$1::vector`, `CAST(? AS VECTOR(n))`) declares it once.
|
|
43
47
|
*/
|
|
44
48
|
protected appendVectorValue(ctx: QueryContext, value: readonly unknown[], _field?: FieldOptions): void;
|
|
45
49
|
/**
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { unsupportedVectorMetric } from '../type/vector.js';
|
|
2
|
-
import { findVectorIndex, findVectorSort } from '../util/dialect.util.js';
|
|
1
|
+
import { DEFAULT_VECTOR_DISTANCE, unsupportedVectorMetric } from '../type/vector.js';
|
|
2
|
+
import { findVectorIndex, findVectorSort, vectorCandidates } from '../util/dialect.util.js';
|
|
3
3
|
import { entityName } from '../util/object.util.js';
|
|
4
4
|
import { AbstractDialect } from './abstractDialect.js';
|
|
5
|
+
import { encodeFloat32s } from './vectorCast.js';
|
|
5
6
|
/**
|
|
6
7
|
* Vector search for the SQL dialects: the distance a `$sort` ranks by and projects, and the ANN tuning.
|
|
7
8
|
* Each dialect lists its metrics in {@link vectorMetrics}, an operator or a function; empty means no search.
|
|
@@ -24,22 +25,26 @@ export class VectorSqlDialect extends AbstractDialect {
|
|
|
24
25
|
* spelled into a `SET` rather than bound, and `/http` input is untyped.
|
|
25
26
|
*/
|
|
26
27
|
tunedVectorIndex(meta, q) {
|
|
27
|
-
|
|
28
|
-
if (candidates === undefined) {
|
|
28
|
+
if (vectorCandidates(q) === undefined) {
|
|
29
29
|
return undefined;
|
|
30
30
|
}
|
|
31
|
-
if (!Number.isInteger(candidates) || candidates < 1) {
|
|
32
|
-
throw new TypeError(`$candidates must be a positive integer, got ${JSON.stringify(candidates)}`);
|
|
33
|
-
}
|
|
34
31
|
const key = this.vectorSortKey(q);
|
|
35
32
|
return key ? findVectorIndex(meta, key) : undefined;
|
|
36
33
|
}
|
|
34
|
+
/** The `$where` a read runs: the query's own, which an engine reading its vector index as a table narrows. */
|
|
35
|
+
rankedWhere(_meta, q, _prefix) {
|
|
36
|
+
return q.$where;
|
|
37
|
+
}
|
|
37
38
|
/**
|
|
38
39
|
* Every distance metric this dialect has, and how a search and an index spell each. Empty means no
|
|
39
40
|
* vector search at all, which is what MySQL and D1 are. The key set is the single answer to "is this
|
|
40
41
|
* metric supported here", for a query and an index alike.
|
|
41
42
|
*/
|
|
42
43
|
vectorMetrics = new Map();
|
|
44
|
+
/** Whether this engine has a vector index: the one a metric's `index` names. */
|
|
45
|
+
hasVectorIndex() {
|
|
46
|
+
return [...this.vectorMetrics.values()].some((metric) => metric.index);
|
|
47
|
+
}
|
|
43
48
|
/**
|
|
44
49
|
* What a distance expression reads, for a `$sort` and a `$near` alike. The metric falls back to the
|
|
45
50
|
* field's, then its index's, which serves no other, then cosine.
|
|
@@ -47,15 +52,15 @@ export class VectorSqlDialect extends AbstractDialect {
|
|
|
47
52
|
resolveVectorDistance(meta, key, search) {
|
|
48
53
|
const field = meta.fields[key];
|
|
49
54
|
const colName = this.resolveColumnName(key, field);
|
|
50
|
-
const distance = search.$distance ?? field?.distance ?? findVectorIndex(meta, key)?.distance ??
|
|
55
|
+
const distance = search.$distance ?? field?.distance ?? findVectorIndex(meta, key)?.distance ?? DEFAULT_VECTOR_DISTANCE;
|
|
51
56
|
return { colName, distance, field };
|
|
52
57
|
}
|
|
53
58
|
/**
|
|
54
59
|
* Binds a vector, both as a persisted value and as the query vector of a distance expression, so a
|
|
55
|
-
* dialect needing a conversion around it (`$1::vector`, `
|
|
60
|
+
* dialect needing a conversion around it (`$1::vector`, `CAST(? AS VECTOR(n))`) declares it once.
|
|
56
61
|
*/
|
|
57
62
|
appendVectorValue(ctx, value, _field) {
|
|
58
|
-
ctx.addValue(`[${value.join(',')}]`);
|
|
63
|
+
ctx.addValue(this.features.vectorBytes ? encodeFloat32s(value) : `[${value.join(',')}]`);
|
|
59
64
|
}
|
|
60
65
|
/**
|
|
61
66
|
* The vector type this dialect actually has for a declared one, so the cast follows the column
|
|
@@ -7,6 +7,6 @@ import type { VectorDistance, VectorMetric } from '../type/index.js';
|
|
|
7
7
|
* functions, which `TursoDialect` inherits.
|
|
8
8
|
*/
|
|
9
9
|
export declare class LibsqlDialect extends SqliteDialect {
|
|
10
|
-
/** libSQL's built-in vector functions; no `inner` (only the Rust engine has it) and no `l1`. */
|
|
10
|
+
/** libSQL's built-in vector functions, and the metric its DiskANN index names; no `inner` (only the Rust engine has it) and no `l1`. */
|
|
11
11
|
readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
12
12
|
}
|
|
@@ -6,9 +6,9 @@ import { SqliteDialect } from '../sqlite/sqliteDialect.js';
|
|
|
6
6
|
* functions, which `TursoDialect` inherits.
|
|
7
7
|
*/
|
|
8
8
|
export class LibsqlDialect extends SqliteDialect {
|
|
9
|
-
/** libSQL's built-in vector functions; no `inner` (only the Rust engine has it) and no `l1`. */
|
|
9
|
+
/** libSQL's built-in vector functions, and the metric its DiskANN index names; no `inner` (only the Rust engine has it) and no `l1`. */
|
|
10
10
|
vectorMetrics = new Map([
|
|
11
|
-
['cosine', { fn: 'vector_distance_cos' }],
|
|
12
|
-
['l2', { fn: 'vector_distance_l2' }],
|
|
11
|
+
['cosine', { fn: 'vector_distance_cos', index: 'cosine' }],
|
|
12
|
+
['l2', { fn: 'vector_distance_l2', index: 'l2' }],
|
|
13
13
|
]);
|
|
14
14
|
}
|
|
@@ -6,8 +6,9 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
6
6
|
readonly dialectName = "mariadb";
|
|
7
7
|
readonly insertIdSource = "returning";
|
|
8
8
|
/**
|
|
9
|
-
* Unlike MySQL: `VECTOR(n)` takes its dimension
|
|
10
|
-
* `CREATE INDEX` takes `IF NOT EXISTS`, and a lock cannot be narrowed
|
|
9
|
+
* Unlike MySQL: `VECTOR(n)` takes its dimension and binds as packed float32 bytes, every column of a
|
|
10
|
+
* vector index has to be NOT NULL, `CREATE INDEX` takes `IF NOT EXISTS`, and a lock cannot be narrowed
|
|
11
|
+
* to one table of a join.
|
|
11
12
|
*/
|
|
12
13
|
readonly features: SqlDialectFeatures;
|
|
13
14
|
/**
|
|
@@ -34,13 +35,6 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
34
35
|
protected jsonDiffers(elem: string, operand: string): string;
|
|
35
36
|
/** `VEC_DISTANCE_COSINE`/`VEC_DISTANCE_EUCLIDEAN`, 11.7+, which the index's `DISTANCE=` names alike. */
|
|
36
37
|
readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
37
|
-
/**
|
|
38
|
-
* A `VECTOR` column holds a packed little-endian float32 blob, and MariaDB refuses text where one
|
|
39
|
-
* belongs: inserting `'[1,2,3]'` fails with `Incorrect vector value`, and passing it to
|
|
40
|
-
* `VEC_DISTANCE_COSINE` with `Illegal parameter data type varchar`. `VEC_FromText` is the
|
|
41
|
-
* conversion, needed on both paths.
|
|
42
|
-
*/
|
|
43
|
-
protected appendVectorValue(ctx: QueryContext, value: readonly unknown[]): void;
|
|
44
38
|
/**
|
|
45
39
|
* `mhnsw_ef_search` too, where a vector search is tuned. A setting scoped to one statement needs
|
|
46
40
|
* neither a transaction nor a restore, and cannot leak to the next query on this pooled connection,
|