uql-orm 0.70.0 → 0.72.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 (78) hide show
  1. package/README.md +2 -0
  2. package/dist/browser/uql-browser.min.js +2 -2
  3. package/dist/browser/uql-browser.min.js.map +3 -3
  4. package/dist/cockroachdb/cockroachDialect.d.ts +6 -0
  5. package/dist/cockroachdb/cockroachDialect.js +8 -0
  6. package/dist/d1/d1SqliteDialect.d.ts +3 -0
  7. package/dist/d1/d1SqliteDialect.js +3 -1
  8. package/dist/dialect/abstractSqlDialect.d.ts +14 -2
  9. package/dist/dialect/abstractSqlDialect.js +48 -16
  10. package/dist/dialect/aliases.d.ts +2 -0
  11. package/dist/dialect/aliases.js +2 -0
  12. package/dist/dialect/hydrateColumn.d.ts +3 -2
  13. package/dist/dialect/hydrateColumn.js +10 -1
  14. package/dist/dialect/mysqlLikeSqlDialect.d.ts +2 -0
  15. package/dist/dialect/mysqlLikeSqlDialect.js +6 -1
  16. package/dist/dialect/pgLikeSqlDialect.d.ts +21 -5
  17. package/dist/dialect/pgLikeSqlDialect.js +48 -15
  18. package/dist/dialect/queryJoins.d.ts +14 -4
  19. package/dist/dialect/queryJoins.js +31 -11
  20. package/dist/dialect/vectorCast.d.ts +2 -0
  21. package/dist/dialect/vectorCast.js +7 -0
  22. package/dist/dialect/vectorSqlDialect.d.ts +7 -3
  23. package/dist/dialect/vectorSqlDialect.js +15 -10
  24. package/dist/libsql/libsqlDialect.d.ts +1 -1
  25. package/dist/libsql/libsqlDialect.js +3 -3
  26. package/dist/maria/mariaDialect.d.ts +3 -9
  27. package/dist/maria/mariaDialect.js +5 -15
  28. package/dist/maria/mariadbQuerier.js +9 -3
  29. package/dist/migrate/ddl/index.d.ts +1 -0
  30. package/dist/migrate/ddl/index.js +4 -2
  31. package/dist/migrate/ddl/indexDdl.d.ts +2 -0
  32. package/dist/migrate/ddl/indexDdl.js +10 -2
  33. package/dist/migrate/ddl/pgIndexDdl.d.ts +3 -4
  34. package/dist/migrate/ddl/pgIndexDdl.js +11 -11
  35. package/dist/migrate/ddl/sqliteIndexDdl.d.ts +11 -0
  36. package/dist/migrate/ddl/sqliteIndexDdl.js +38 -0
  37. package/dist/migrate/generator/mongoCommand.d.ts +28 -0
  38. package/dist/migrate/generator/mongoCommand.js +8 -0
  39. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +2 -0
  40. package/dist/migrate/generator/mongoSchemaGenerator.js +50 -6
  41. package/dist/migrate/introspection/mongoIntrospector.js +33 -7
  42. package/dist/migrate/introspection/postgresIntrospector.js +19 -0
  43. package/dist/migrate/introspection/sqliteIntrospector.d.ts +6 -0
  44. package/dist/migrate/introspection/sqliteIntrospector.js +29 -3
  45. package/dist/mongo/mongoDialect.d.ts +5 -3
  46. package/dist/mongo/mongoDialect.js +40 -17
  47. package/dist/mongo/mongodbQuerier.js +4 -5
  48. package/dist/mssql/mssqlDialect.js +1 -0
  49. package/dist/schema/canonicalType.js +10 -4
  50. package/dist/schema/indexDifferences.d.ts +5 -2
  51. package/dist/schema/indexDifferences.js +5 -0
  52. package/dist/schema/schemaASTBuilder.js +3 -2
  53. package/dist/sqlite/localSqliteQuerierPool.d.ts +10 -11
  54. package/dist/sqlite/localSqliteQuerierPool.js +8 -11
  55. package/dist/sqlite/nodeSqliteQuerierPool.d.ts +4 -2
  56. package/dist/sqlite/nodeSqliteQuerierPool.js +7 -4
  57. package/dist/sqlite/sqliteDialect.d.ts +11 -1
  58. package/dist/sqlite/sqliteDialect.js +46 -3
  59. package/dist/sqlite/sqliteQuerierPool.d.ts +6 -5
  60. package/dist/sqlite/sqliteQuerierPool.js +10 -7
  61. package/dist/turso/tursoLocalDialect.d.ts +1 -1
  62. package/dist/turso/tursoLocalDialect.js +1 -1
  63. package/dist/turso/tursoLocalQuerierPool.d.ts +4 -5
  64. package/dist/turso/tursoLocalQuerierPool.js +3 -10
  65. package/dist/type/dialect.d.ts +5 -0
  66. package/dist/type/entity.d.ts +27 -13
  67. package/dist/type/migration.d.ts +7 -9
  68. package/dist/type/query.d.ts +11 -4
  69. package/dist/type/queryAggregate.d.ts +5 -16
  70. package/dist/type/utility.d.ts +13 -0
  71. package/dist/type/vector.d.ts +17 -0
  72. package/dist/type/vector.js +6 -0
  73. package/dist/util/ddlExpression.util.d.ts +2 -0
  74. package/dist/util/ddlExpression.util.js +5 -0
  75. package/dist/util/dialect.util.d.ts +21 -2
  76. package/dist/util/dialect.util.js +42 -2
  77. package/package.json +4 -3
  78. package/skills/uql-orm/SKILL.md +146 -0
@@ -1,7 +1,7 @@
1
1
  import { TursoDialect } from './tursoDialect.js';
2
2
  /**
3
3
  * SQLite Dialect specialization for the embedded Turso engine, the Rust engine alone: it adds a
4
- * dot-product distance to libSQL's cosine and L2, and caps no function call.
4
+ * dot-product distance to libSQL's cosine and L2, caps no function call, and has no vector index.
5
5
  */
6
6
  export class TursoLocalDialect extends TursoDialect {
7
7
  vectorMetrics = new Map([
@@ -1,15 +1,14 @@
1
1
  import type { connect } from '@tursodatabase/database';
2
- import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
3
- import { type SqliteDatabase, SqliteQuerier } from '../sqlite/sqliteQuerier.js';
2
+ import { AbstractLocalSqliteQuerierPool } from '../sqlite/localSqliteQuerierPool.js';
3
+ import type { SqliteDatabase } from '../sqlite/sqliteQuerier.js';
4
4
  import type { ExtraOptions } from '../type/index.js';
5
5
  import { TursoLocalDialect } from './tursoLocalDialect.js';
6
6
  /** The engine's own options: `readonly`, `timeout`, `encryption`, `experimental` and the rest. */
7
7
  export type TursoLocalOptions = NonNullable<Parameters<typeof connect>[1]>;
8
8
  /** A pool for the embedded Turso engine, on `uql-orm/turso/local` so its native binaries stay out of edge bundles. */
9
- export declare class TursoLocalQuerierPool extends AbstractSharedHandleQuerierPool<SqliteDatabase, SqliteQuerier, TursoLocalDialect> {
9
+ export declare class TursoLocalQuerierPool extends AbstractLocalSqliteQuerierPool<SqliteDatabase, TursoLocalDialect> {
10
10
  readonly filename: string;
11
11
  readonly opts?: TursoLocalOptions | undefined;
12
12
  constructor(filename?: string, opts?: TursoLocalOptions | undefined, extra?: ExtraOptions);
13
- protected openDb(): Promise<SqliteDatabase>;
14
- protected buildQuerier(db: SqliteDatabase): SqliteQuerier;
13
+ protected createDb(): Promise<SqliteDatabase>;
15
14
  }
@@ -1,10 +1,8 @@
1
1
  import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
2
- import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
3
- import { applySqlitePragmas } from '../sqlite/sqlitePragmas.js';
4
- import { SqliteQuerier } from '../sqlite/sqliteQuerier.js';
2
+ import { AbstractLocalSqliteQuerierPool } from '../sqlite/localSqliteQuerierPool.js';
5
3
  import { TursoLocalDialect } from './tursoLocalDialect.js';
6
4
  /** A pool for the embedded Turso engine, on `uql-orm/turso/local` so its native binaries stay out of edge bundles. */
7
- export class TursoLocalQuerierPool extends AbstractSharedHandleQuerierPool {
5
+ export class TursoLocalQuerierPool extends AbstractLocalSqliteQuerierPool {
8
6
  filename;
9
7
  opts;
10
8
  constructor(filename = ':memory:', opts, extra) {
@@ -12,15 +10,10 @@ export class TursoLocalQuerierPool extends AbstractSharedHandleQuerierPool {
12
10
  this.filename = filename;
13
11
  this.opts = opts;
14
12
  }
15
- async openDb() {
13
+ async createDb() {
16
14
  const { connect } = await import('@tursodatabase/database');
17
15
  const db = await connect(this.filename, this.opts);
18
- // Integers as `bigint`, which the querier decodes exactly past 2^53.
19
16
  db.defaultSafeIntegers(true);
20
- await applySqlitePragmas(db);
21
17
  return db;
22
18
  }
23
- buildQuerier(db) {
24
- return new SqliteQuerier(db, this.dialect, this.extra);
25
- }
26
19
  }
@@ -107,6 +107,11 @@ export interface DialectFeatures {
107
107
  readonly vectorIndexRequiresNotNull: boolean;
108
108
  /** Whether the dialect requires/allows (n) length constraints on vector types. */
109
109
  readonly vectorSupportsLength: boolean;
110
+ /**
111
+ * Whether a vector binds as its packed little-endian float32 bytes, which a blob column holds and the
112
+ * SQLite family and MariaDB read, rather than as `[1,2,3]` text they would reparse per row.
113
+ */
114
+ readonly vectorBytes: boolean;
110
115
  /** Whether the dialect natively supports the TIMESTAMPTZ alias/type. */
111
116
  readonly supportsTimestamptz: boolean;
112
117
  /**
@@ -2,7 +2,7 @@ import type { EnumValues, ForeignKeyAction, IndexType } from '../schema/types.js
2
2
  import type { FilterOptions, RelationQuery } from './query.js';
3
3
  import type { ColumnRef, QueryRaw, RelationAggregate } from './queryRaw.js';
4
4
  import type { QueryWhere } from './queryWhere.js';
5
- import type { Except, IsMany, Json, Scalar, Type, Unpacked, Writable } from './utility.js';
5
+ import type { Except, ExactlyOne, IsEqual, IsMany, Json, Scalar, Type, Unpacked, Writable } from './utility.js';
6
6
  import type { VectorDistance, VectorIndexOptions, VectorIndexType } from './vector.js';
7
7
  /** Brands the property an entity is identified by, where it is not `id`, `_id` or `uuid`. */
8
8
  export declare const idKey: unique symbol;
@@ -20,19 +20,13 @@ export type Key<E> = keyof E & string;
20
20
  export type FieldKey<E> = {
21
21
  readonly [K in keyof E]-?: [NonNullable<E[K]>] extends [Scalar | readonly Scalar[] | Json | readonly Json[]] ? K : never;
22
22
  }[Key<E>];
23
- /**
24
- * Whether `A` and `B` are the same type, `readonly` included - which no conditional sees, since
25
- * assignability ignores the modifier. Two identical generic signatures compare equal only when their
26
- * deferred bodies do.
27
- */
28
- type IfEquals<A, B, Yes, No> = (<T>() => T extends A ? 1 : 2) extends <T>() => (T extends B ? 1 : 2) ? Yes : No;
29
23
  /**
30
24
  * The fields a caller writes: every one the class does not declare `readonly`. A field the database
31
25
  * writes - a relation aggregate, a stored generated column, a trigger-kept stamp - is `readonly`, and
32
26
  * its value never reaches the database, so a write payload leaves it out rather than dropping it.
33
27
  */
34
28
  export type WritableKey<E> = {
35
- readonly [K in FieldKey<E>]-?: IfEquals<Pick<E, K>, Writable<Pick<E, K>>, K, never>;
29
+ readonly [K in FieldKey<E>]-?: IsEqual<Pick<E, K>, Writable<Pick<E, K>>> extends true ? K : never;
36
30
  }[FieldKey<E>];
37
31
  /** A whole-record write as a caller supplies one: {@link EntityData} without the fields it cannot write. */
38
32
  export type EntityWrite<E> = EntityData<E, WritableKey<E>>;
@@ -110,8 +104,17 @@ export type JsonUpdateOp<T = unknown> = {
110
104
  * operators would address keys it does not have (engines disagree on what that does).
111
105
  */
112
106
  type JsonUpdateOpFor<V, T = UnwrapJson<NonNullable<V>>> = [T] extends [never] ? never : IsMany<T> extends true ? never : JsonUpdateOp<T>;
113
- /** What an update takes beyond the value: `null` to clear an optional member, `raw` SQL, and JSON operators. */
114
- type UpdateExtra<V, Raw> = (undefined extends V ? null : never) | Raw | JsonUpdateOpFor<V>;
107
+ /**
108
+ * A scalar field's update operator, as {@link JsonUpdateOp} is a JSON field's, computed in the statement:
109
+ * `$inc` adds, `$mul` multiplies, a NULL counting as 0 on every engine. One per field, since their order
110
+ * would change the result. A `bigint` steps by a `bigint`, exactly.
111
+ * @example `{ stock: { $inc: -1 } }`
112
+ */
113
+ export type FieldUpdateOp<T extends number | bigint = number | bigint> = ExactlyOne<Record<'$inc' | '$mul', T>>;
114
+ /** The {@link FieldUpdateOp} a field takes: `never` on one it has no operator for, which is any but a number. */
115
+ type FieldUpdateOpFor<V> = [NonNullable<V>] extends [number] ? FieldUpdateOp<number> : [NonNullable<V>] extends [bigint] ? FieldUpdateOp<bigint> : never;
116
+ /** What an update takes beyond the value: `null` to clear an optional member, `raw` SQL, and update operators. */
117
+ type UpdateExtra<V, Raw> = (undefined extends V ? null : never) | Raw | JsonUpdateOpFor<V> | FieldUpdateOpFor<V>;
115
118
  /**
116
119
  * What a whole-record write persists: the fields and relations with their declared optionality, a
117
120
  * related row's alike, and no methods. Two mapped types, since asking each key costs a conditional.
@@ -504,15 +507,26 @@ export type HookRegistration = {
504
507
  readonly methodName: string;
505
508
  };
506
509
  /**
507
- * An index type with the metric it needs: a vector index has to name one, since engines default to
508
- * a different one than the queries use, and any other index names none.
510
+ * An index type with what it needs: a vector index has to name its metric, since engines default to a
511
+ * different one than the queries use; an Atlas `vectorSearch` index may, else takes its field's, else
512
+ * cosine; a `fulltext` one may name the text-search `config` it parses with; and any other index neither.
509
513
  */
510
514
  export type IndexTypeOptions = {
511
515
  type: VectorIndexType;
512
516
  distance: VectorDistance;
517
+ config?: never;
518
+ } | {
519
+ type: 'vectorSearch';
520
+ distance?: VectorDistance;
521
+ config?: never;
522
+ } | {
523
+ type: 'fulltext';
524
+ distance?: never;
525
+ config?: string;
513
526
  } | {
514
- type?: Exclude<IndexType, VectorIndexType>;
527
+ type?: Exclude<IndexType, VectorIndexType | 'vectorSearch' | 'fulltext'>;
515
528
  distance?: never;
529
+ config?: never;
516
530
  };
517
531
  /** One index entry as the migration builder takes it: a column name, `raw`, or an object when it needs more. */
518
532
  export type IndexColumnInput = string | QueryRaw | EntityIndexColumn;
@@ -1,9 +1,8 @@
1
- import type { VectorCast } from '../dialect/vectorCast.js';
2
1
  import type { AnyMigrationOperation } from '../migrate/builder/types.js';
3
2
  import type { IndexFacet } from '../schema/indexDifferences.js';
4
3
  import type { SchemaAST } from '../schema/schemaAST.js';
5
4
  import type { ColumnNode, ForeignKeyAction, IndexType, TableNode } from '../schema/types.js';
6
- import type { EntityMeta, EntityWhereMeta, FieldOptions, IndexColumnSchema, LoggingOptions, Querier, SqlQuerier, Type, VectorIndexOptions } from './index.js';
5
+ import type { EntityMeta, EntityWhereMeta, FieldOptions, IndexColumnSchema, IndexedVectorField, LoggingOptions, Querier, SqlQuerier, Type, VectorIndexOptions } from './index.js';
7
6
  /**
8
7
  * Defines a migration using a simple object literal. `Q` is `MongoQuerier` for a MongoDB migration.
9
8
  */
@@ -133,7 +132,12 @@ export interface TableSchema {
133
132
  /**
134
133
  * Represents an index in a database table
135
134
  */
136
- export interface IndexSchema extends VectorIndexOptions {
135
+ export interface IndexSchema extends VectorIndexOptions, IndexedVectorField {
136
+ /**
137
+ * A `fulltext` index's text-search configuration, which `$text` parses with on the Postgres family
138
+ * (`'english'`); `'simple'` where unstated. Other engines take their language from elsewhere.
139
+ */
140
+ readonly config?: string;
137
141
  readonly name: string;
138
142
  /**
139
143
  * What the index is over, in order. Named `entries` and not `columns` because an entry need not be
@@ -149,12 +153,6 @@ export interface IndexSchema extends VectorIndexOptions {
149
153
  readonly where?: string;
150
154
  /** Non-key columns stored in the index (Postgres-wire `INCLUDE`). */
151
155
  readonly include?: readonly string[];
152
- /**
153
- * The indexed column's vector type, which pgvector's operator-class names are built from
154
- * (`halfvec_cosine_ops`). Absent for a non-vector index, and for an index whose column types are
155
- * unknown, where `vector` is assumed.
156
- */
157
- readonly vectorType?: VectorCast;
158
156
  }
159
157
  /**
160
158
  * A foreign key constraint, wherever one is described: read back by introspection, planned into a
@@ -130,15 +130,22 @@ export type QuerySortByCount = {
130
130
  $count: QuerySortDirection;
131
131
  };
132
132
  /**
133
- * A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count`, or a vector distance,
134
- * which `Vector` confines to the queried entity. One mapped type over the key sets: an intersection is
135
- * checked once per member, which made this the costliest type to check.
133
+ * Ordering by relevance to the `$text` at the root of `$where`: most relevant first, the one order every
134
+ * engine ranks by (MongoDB's `textScore` sorts no other way).
135
+ */
136
+ export type QuerySortByText = {
137
+ $text?: -1 | 'desc';
138
+ };
139
+ /**
140
+ * A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count`, or a vector distance or
141
+ * `$text` relevance, which `Vector` confines to the queried entity. One mapped type over the key sets: an
142
+ * intersection is checked once per member, which made this the costliest type to check.
136
143
  */
137
144
  export type QuerySortMap<E, Vector extends boolean = true, K extends keyof E = FieldKey<E> | RelationKey<E>> = {
138
145
  [P in K]?: P extends RelationKey<E> ? IsMany<E[P]> extends true ? QuerySortByCount : QuerySortMap<RelationTarget<E[P]>, false> : Vector extends true ? NonNullable<E[P]> extends readonly number[] ? QuerySortValue : QuerySortDirection : QuerySortDirection;
139
146
  } & ([JsonFieldPaths<E>] extends [never] ? unknown : {
140
147
  [P in JsonFieldPaths<E>]?: QuerySortDirection;
141
- });
148
+ }) & (Vector extends true ? QuerySortByText : unknown);
142
149
  /**
143
150
  * pager options.
144
151
  */
@@ -2,7 +2,7 @@ import type { FieldKey, RelationKey, RelationTarget } from './entity.js';
2
2
  import type { QueryPager, QuerySelect, QuerySortDirection } from './query.js';
3
3
  import type { QueryRaw } from './queryRaw.js';
4
4
  import type { QueryWhere, QueryWhereFieldValue } from './queryWhere.js';
5
- import type { IsMany, RejectKeys } from './utility.js';
5
+ import type { ExactlyOne, IsMany, RejectKeys } from './utility.js';
6
6
  /** The columns `$group` names by a literal `true`, so an uninferred `$group`, its own constraint, names none. */
7
7
  type GroupedKeys<G> = {
8
8
  [K in keyof G]: G[K] extends true ? K : never;
@@ -37,13 +37,6 @@ export declare function resolveAggregateOp(key: string): {
37
37
  op: QueryAggregateOp;
38
38
  distinct: boolean;
39
39
  };
40
- /**
41
- * Exactly one key of `T` with its value; every other key is forbidden (`never`). `Pick`, not `Record`,
42
- * so the chosen key stays linked to `T`'s own property and renames follow it through.
43
- */
44
- type ExactlyOne<T> = {
45
- [K in keyof T]: Readonly<Pick<T, K>> & Partial<Readonly<Record<Exclude<keyof T, K>, never>>>;
46
- }[keyof T];
47
40
  /**
48
41
  * One field named as a key - `{ amount: true }` - the way a statement names every field, so an editor
49
42
  * rename reaches it where a string never would. `F` narrows which fields qualify.
@@ -122,16 +115,12 @@ type QueryGroupSchema<E, G> = Readonly<QuerySelect<E, FieldKey<E>, true>> & {
122
115
  readonly [K in Exclude<NamedKeys<G>, FieldKey<E>>]: QueryGroupRef<E>;
123
116
  } & RejectKeys<Exclude<GroupedKeys<G>, FieldKey<E>>>;
124
117
  /**
125
- * The value a path reads: the field's own type, or through a relation, `null` where the row points nowhere.
126
- * `any` answers `unknown`: TypeScript checks a deferred type by instantiating it with `any`, which would
127
- * walk every relation of the entity on every aggregate call, quadrupling what one costs to check.
118
+ * The value a path reads: the field at its end as its column holds it, `null` only where that does, since
119
+ * the path joins the rows it reads. `any` answers `unknown`: TypeScript checks a deferred type by
120
+ * instantiating it with `any`, which would walk every relation of the entity on every aggregate call.
128
121
  */
129
122
  type GroupRefValue<E, Ref> = 0 extends 1 & Ref ? unknown : {
130
- [K in keyof Ref & keyof E]: Ref[K] extends true ? E[K] : NonNullable<GroupRefLeaf<RelationTarget<E[K]>, Ref[K]>> | null;
131
- }[keyof Ref & keyof E];
132
- /** The field at the end of a path, whose type {@link GroupRefValue} widens with `null`; `any` as it does. */
133
- type GroupRefLeaf<E, Ref> = 0 extends 1 & Ref ? unknown : {
134
- [K in keyof Ref & keyof E]: Ref[K] extends true ? E[K] : GroupRefLeaf<RelationTarget<E[K]>, Ref[K]>;
123
+ [K in keyof Ref & keyof E]: Ref[K] extends true ? Exclude<E[K], undefined> : GroupRefValue<RelationTarget<E[K]>, Ref[K]>;
135
124
  }[keyof Ref & keyof E];
136
125
  /** Computed columns by the alias each is read back under: `{ count: { $count: '*' }, avgAge: { $avg: { age: true } } }`. */
137
126
  export type QueryAggMap<E> = {
@@ -30,6 +30,12 @@ export type ExpandScalar<T> = T extends Date ? Date | string : T;
30
30
  export interface RawRow {
31
31
  [key: string]: unknown;
32
32
  }
33
+ /**
34
+ * Whether `A` and `B` are the same type, `readonly` included - which no conditional sees, since
35
+ * assignability ignores the modifier. Two identical generic signatures compare equal only when their
36
+ * deferred bodies do.
37
+ */
38
+ export type IsEqual<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => (T extends B ? 1 : 2) ? true : false;
33
39
  export type Writable<T> = {
34
40
  -readonly [K in keyof T]: T[K];
35
41
  };
@@ -46,6 +52,13 @@ export type Except<T, K extends keyof T> = {
46
52
  * excess-property check on one.
47
53
  */
48
54
  export type RejectKeys<K> = [K] extends [never] ? unknown : Record<K & string, never>;
55
+ /**
56
+ * Exactly one key of `T` with its value; every other key is forbidden (`never`). `Pick`, not `Record`,
57
+ * so the chosen key stays linked to `T`'s own property and renames follow it through.
58
+ */
59
+ export type ExactlyOne<T> = {
60
+ [K in keyof T]: Readonly<Pick<T, K>> & Partial<Readonly<Record<Exclude<keyof T, K>, never>>>;
61
+ }[keyof T];
49
62
  export type Unpacked<T> = T extends readonly (infer U)[] ? U : T extends (...args: unknown[]) => infer U ? U : T extends Promise<infer U> ? U : T;
50
63
  /**
51
64
  * Whether the value a property holds is many rather than one: a to-many relation, a scalar array, a
@@ -1,3 +1,4 @@
1
+ import type { VectorCast } from '../dialect/vectorCast.js';
1
2
  import type { IndexType } from '../schema/types.js';
2
3
  /**
3
4
  * A vector search's metric: `cosine` (the default), `l2`, `inner` product or `l1`. No hamming: every
@@ -55,6 +56,22 @@ export type VectorIndexOptions = {
55
56
  /** IVFFlat: number of inverted lists. */
56
57
  lists?: number;
57
58
  };
59
+ /** The metric a search or an index measures by where nothing names one: every engine with vectors has it. */
60
+ export declare const DEFAULT_VECTOR_DISTANCE: VectorDistance;
61
+ /** The metric an index is built for: its own, else {@link DEFAULT_VECTOR_DISTANCE}. */
62
+ export declare function indexDistance(index: {
63
+ readonly distance?: VectorDistance;
64
+ }): VectorDistance;
65
+ /**
66
+ * What a vector index's DDL reads off the field it indexes, never declared on the index itself, so the
67
+ * two cannot disagree. Absent for a non-vector index, and for one whose field is unknown.
68
+ */
69
+ export type IndexedVectorField = {
70
+ /** The field's vector type, which pgvector's operator classes are named after (`halfvec_cosine_ops`); `vector` where absent. */
71
+ readonly vectorType?: VectorCast;
72
+ /** The field's dimensions, which an Atlas vector search index states. */
73
+ readonly dimensions?: number;
74
+ };
58
75
  /**
59
76
  * Index types whose emitted DDL depends on the distance metric. The runtime list is the source, so
60
77
  * the type and every dialect's "do I have this one?" answer cannot drift from each other.
@@ -5,6 +5,12 @@ export function unsupportedVectorMetric(dialectName, distance, indexName) {
5
5
  const where = indexName === undefined ? '' : ` (index "${indexName}")`;
6
6
  return new TypeError(`${dialectName} does not support vector distance metric: ${distance}${where}`);
7
7
  }
8
+ /** The metric a search or an index measures by where nothing names one: every engine with vectors has it. */
9
+ export const DEFAULT_VECTOR_DISTANCE = 'cosine';
10
+ /** The metric an index is built for: its own, else {@link DEFAULT_VECTOR_DISTANCE}. */
11
+ export function indexDistance(index) {
12
+ return index.distance ?? DEFAULT_VECTOR_DISTANCE;
13
+ }
8
14
  /**
9
15
  * Index types whose emitted DDL depends on the distance metric. The runtime list is the source, so
10
16
  * the type and every dialect's "do I have this one?" answer cannot drift from each other.
@@ -10,3 +10,5 @@ export declare function declaredIndexes<E>(meta: EntityMeta<E>): EntityIndexMeta
10
10
  export declare function renderIndexColumn(entry: EntityIndexColumn, render: (sql: QueryRaw) => string): IndexColumnSchema;
11
11
  /** What an unnamed index's name is built from: each entry's column, or `expr<n>` for an expression, which has none. */
12
12
  export declare function indexNameParts(entries: readonly EntityIndexColumn[]): string[];
13
+ /** The name an index is created and read by: its own, else one derived from its table and its entries' columns. */
14
+ export declare function declaredIndexName(name: string | undefined, table: string, entries: readonly EntityIndexColumn[]): string;
@@ -1,5 +1,6 @@
1
1
  import { ColumnRef, QueryRaw, } from '../type/index.js';
2
2
  import { definedEntries } from './object.util.js';
3
+ import { derivedIndexName } from './sql.util.js';
3
4
  /**
4
5
  * Reduces an authored index entry to the form metadata keeps, so a column, an expression and an options
5
6
  * object reach the schema as one: a column read off the refs as its key, any other `raw` as it is.
@@ -30,3 +31,7 @@ export function renderIndexColumn(entry, render) {
30
31
  export function indexNameParts(entries) {
31
32
  return entries.map((entry, at) => (typeof entry.column === 'string' ? entry.column : `expr${at}`));
32
33
  }
34
+ /** The name an index is created and read by: its own, else one derived from its table and its entries' columns. */
35
+ export function declaredIndexName(name, table, entries) {
36
+ return name ?? derivedIndexName(table, indexNameParts(entries));
37
+ }
@@ -1,4 +1,4 @@
1
- import { type CascadeType, type EntityData, type EntityId, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorSearch, type QueryWhere, type RelationKey, type UpdatePayload } from '../type/index.js';
1
+ import { type CascadeType, type EntityData, type EntityId, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type FieldUpdateOp, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorSearch, type QueryWhere, type RelationKey, type UpdatePayload } from '../type/index.js';
2
2
  export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
3
3
  /** The keys of `payload` a write persists as columns. */
4
4
  export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E> | UpdatePayload<E>, callbackKey: CallbackKey): FieldKey<E>[];
@@ -71,6 +71,10 @@ export declare function findVectorSort<E>(sort: QuerySortMap<E> | undefined): {
71
71
  key: string;
72
72
  search: QueryVectorSearch;
73
73
  } | undefined;
74
+ /** `$candidates`, checked: it can be spelled into a statement, and `/http` input is untyped. */
75
+ export declare function vectorCandidates(q: {
76
+ readonly $candidates?: number;
77
+ }): number | undefined;
74
78
  /**
75
79
  * The vector index declared on `key`, if any. Answers both "is there an ANN index to tune here" and
76
80
  * "which kind", which decide the name Atlas is queried by and the setting Postgres is tuned with.
@@ -83,6 +87,10 @@ export declare function findVectorIndex<E>(meta: EntityMeta<E>, key: string): En
83
87
  export declare function hasVectorNear(where: unknown): boolean;
84
88
  /** Type guard: checks whether an update payload value is a JSON operator object. */
85
89
  export declare function isJsonUpdateOp(value: unknown): value is JsonUpdateOp;
90
+ /** Type guard: checks whether an update payload value is a scalar field's operator. */
91
+ export declare function isFieldUpdateOp(value: unknown): value is FieldUpdateOp;
92
+ /** The one operator a scalar field's update carries, and its operand. */
93
+ export declare function fieldUpdateOf(value: FieldUpdateOp): [keyof FieldUpdateOp, number | bigint];
86
94
  /**
87
95
  * The `$where` naming rows by key: a bare value names the one key column (refused on a composite), a
88
96
  * composite's key map is a `$where` already, and a list is an `IN` of bare values or an OR of maps.
@@ -114,7 +122,7 @@ export type ParsedGroupEntry<E = object> = {
114
122
  readonly alias: string;
115
123
  readonly op: QueryAggregateOp;
116
124
  readonly fieldRef: string;
117
- /** `true` for a flat distinct op (`$countDistinct`, ...) -> `COUNT(DISTINCT field)`. */
125
+ /** `true` for `$countDistinct`: `COUNT(DISTINCT field)`. */
118
126
  readonly distinct: boolean;
119
127
  /** The rows it reads, where not all of the statement's. */
120
128
  readonly where?: QueryWhere<E>;
@@ -159,6 +167,17 @@ export declare function assertNonNegativeInteger(value: number, clause: string):
159
167
  export declare function throwUnknownAggregateColumn(key: string, clause: string): never;
160
168
  /** {@link throwUnknownAggregateColumn} over every key of a clause, for backends that check up front. */
161
169
  export declare function assertAggregateColumns(clauseMap: object, emitted: ReadonlySet<string>, clause: string): void;
170
+ /** The text-search config a fulltext index builds with, which a search it serves has to parse with too. */
171
+ export declare function fulltextConfig(index: {
172
+ readonly config?: string;
173
+ }): string;
174
+ /** The fulltext index over exactly `fields`, in order, which a search of them is served by. */
175
+ export declare function fulltextIndexOver<E>(meta: EntityMeta<E>, fields: readonly string[]): EntityIndexMeta<E> | undefined;
176
+ /**
177
+ * The search a `$sort` by `$text` ranks by: the one at the root of the same query's `$where`. A nested or
178
+ * negated one has no score to order by, and MongoDB scores only the one `$text` it allows.
179
+ */
180
+ export declare function rankedTextSearch<E>(where: QueryWhere<E> | undefined): QueryTextSearchOptions<E>;
162
181
  /**
163
182
  * The fields a `$text` searches: those it names, or else the columns of the entity's fulltext index,
164
183
  * the declaration MySQL's `MATCH` has to name exactly and a MongoDB text index already is. Refused
@@ -188,6 +188,14 @@ export function findVectorSort(sort) {
188
188
  }
189
189
  return undefined;
190
190
  }
191
+ /** `$candidates`, checked: it can be spelled into a statement, and `/http` input is untyped. */
192
+ export function vectorCandidates(q) {
193
+ const candidates = q.$candidates;
194
+ if (candidates !== undefined && (!Number.isInteger(candidates) || candidates < 1)) {
195
+ throw new TypeError(`$candidates must be a positive integer, got ${JSON.stringify(candidates)}`);
196
+ }
197
+ return candidates;
198
+ }
191
199
  /**
192
200
  * Every index type that means "vector" to some engine: pgvector's two, the generic one MariaDB and
193
201
  * CockroachDB share, and Atlas's. Wider than {@link VECTOR_INDEX_TYPES}, which is the set whose DDL
@@ -228,6 +236,16 @@ const JSON_UPDATE_OPS = [
228
236
  export function isJsonUpdateOp(value) {
229
237
  return isRecord(value) && someKey(value, (key) => JSON_UPDATE_OPS.includes(key));
230
238
  }
239
+ /** `satisfies` ties this to {@link FieldUpdateOp}, so renaming an operator breaks it at compile time. */
240
+ const FIELD_UPDATE_OPS = ['$inc', '$mul'];
241
+ /** Type guard: checks whether an update payload value is a scalar field's operator. */
242
+ export function isFieldUpdateOp(value) {
243
+ return isRecord(value) && someKey(value, (key) => FIELD_UPDATE_OPS.includes(key));
244
+ }
245
+ /** The one operator a scalar field's update carries, and its operand. */
246
+ export function fieldUpdateOf(value) {
247
+ return value.$inc === undefined ? ['$mul', value.$mul] : ['$inc', value.$inc];
248
+ }
231
249
  /**
232
250
  * The `$where` naming rows by key: a bare value names the one key column (refused on a composite), a
233
251
  * composite's key map is a `$where` already, and a list is an `IN` of bare values or an OR of maps.
@@ -359,10 +377,10 @@ export function parseGroupMap(group, select) {
359
377
  if (key === undefined) {
360
378
  throw new TypeError(`aggregate '${alias}' names no op, only a $where`);
361
379
  }
362
- // Flat DISTINCT ops (`$countDistinct`, ...) normalize to their base op + a `distinct` flag.
380
+ // `$countDistinct` normalizes to `$count` plus a `distinct` flag.
363
381
  const { op, distinct } = resolveAggregateOp(key);
364
382
  const fieldRef = aggregateFieldRef(alias, call[key]);
365
- entries.push({ kind: 'fn', alias, op, fieldRef, distinct, ...(where && hasKeys(where) ? { where } : {}) });
383
+ entries.push({ kind: 'fn', alias, op, fieldRef, distinct, ...(hasKeys(where) ? { where } : {}) });
366
384
  }
367
385
  return entries;
368
386
  }
@@ -432,6 +450,28 @@ export function assertAggregateColumns(clauseMap, emitted, clause) {
432
450
  }
433
451
  }
434
452
  }
453
+ /** The text-search config a fulltext index builds with where it states none: language-neutral, no stemming. */
454
+ const DEFAULT_TEXT_CONFIG = 'simple';
455
+ /** The text-search config a fulltext index builds with, which a search it serves has to parse with too. */
456
+ export function fulltextConfig(index) {
457
+ return index.config ?? DEFAULT_TEXT_CONFIG;
458
+ }
459
+ /** The fulltext index over exactly `fields`, in order, which a search of them is served by. */
460
+ export function fulltextIndexOver(meta, fields) {
461
+ return meta.indexes?.find((index) => index.type === 'fulltext' &&
462
+ index.columns.length === fields.length &&
463
+ index.columns.every((entry, at) => entry.column === fields[at]));
464
+ }
465
+ /**
466
+ * The search a `$sort` by `$text` ranks by: the one at the root of the same query's `$where`. A nested or
467
+ * negated one has no score to order by, and MongoDB scores only the one `$text` it allows.
468
+ */
469
+ export function rankedTextSearch(where) {
470
+ if (!where?.$text) {
471
+ throw new TypeError('$sort by $text ranks by the $text at the root of $where, which this query has none of');
472
+ }
473
+ return where.$text;
474
+ }
435
475
  /**
436
476
  * The fields a `$text` searches: those it names, or else the columns of the entity's fulltext index,
437
477
  * the declaration MySQL's `MATCH` has to name exactly and a MongoDB text index already is. Refused
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.70.0",
6
+ "version": "0.72.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -52,11 +52,12 @@
52
52
  },
53
53
  "files": [
54
54
  "dist",
55
+ "skills",
55
56
  "README.md"
56
57
  ],
57
58
  "scripts": {
58
- "prepack": "bun run build && bun run verify-dist.ts && cp ../../README.md .",
59
- "postpack": "rm README.md && npm pkg delete gitHead",
59
+ "prepack": "bun run build && bun run verify-dist.ts && cp ../../README.md . && cp -R ../../skills .",
60
+ "postpack": "rm -r README.md skills && npm pkg delete gitHead",
60
61
  "compile.browser": "bun build src/browser/index.ts --minify --sourcemap=linked --format=esm --target=browser --outdir=dist/browser --entry-naming 'uql-browser.min.[ext]'",
61
62
  "build": "bun run clean && tsc -b tsconfig.build.json && bun run compile.browser && bun run verify-dist.ts",
62
63
  "start": "tsc --watch",