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.
Files changed (71) hide show
  1. package/README.md +2 -0
  2. package/dist/cockroachdb/cockroachDialect.d.ts +6 -0
  3. package/dist/cockroachdb/cockroachDialect.js +8 -0
  4. package/dist/d1/d1SqliteDialect.d.ts +3 -0
  5. package/dist/d1/d1SqliteDialect.js +3 -1
  6. package/dist/dialect/abstractSqlDialect.d.ts +5 -0
  7. package/dist/dialect/abstractSqlDialect.js +12 -5
  8. package/dist/dialect/hydrateColumn.d.ts +3 -2
  9. package/dist/dialect/hydrateColumn.js +10 -1
  10. package/dist/dialect/mysqlLikeSqlDialect.js +2 -1
  11. package/dist/dialect/pgLikeSqlDialect.d.ts +17 -5
  12. package/dist/dialect/pgLikeSqlDialect.js +33 -15
  13. package/dist/dialect/queryJoins.d.ts +14 -3
  14. package/dist/dialect/queryJoins.js +31 -11
  15. package/dist/dialect/vectorCast.d.ts +2 -0
  16. package/dist/dialect/vectorCast.js +7 -0
  17. package/dist/dialect/vectorSqlDialect.d.ts +7 -3
  18. package/dist/dialect/vectorSqlDialect.js +15 -10
  19. package/dist/libsql/libsqlDialect.d.ts +1 -1
  20. package/dist/libsql/libsqlDialect.js +3 -3
  21. package/dist/maria/mariaDialect.d.ts +3 -9
  22. package/dist/maria/mariaDialect.js +4 -13
  23. package/dist/maria/mariadbQuerier.js +9 -3
  24. package/dist/migrate/ddl/index.d.ts +1 -0
  25. package/dist/migrate/ddl/index.js +4 -2
  26. package/dist/migrate/ddl/indexDdl.d.ts +2 -0
  27. package/dist/migrate/ddl/indexDdl.js +10 -2
  28. package/dist/migrate/ddl/pgIndexDdl.d.ts +3 -4
  29. package/dist/migrate/ddl/pgIndexDdl.js +11 -11
  30. package/dist/migrate/ddl/sqliteIndexDdl.d.ts +11 -0
  31. package/dist/migrate/ddl/sqliteIndexDdl.js +38 -0
  32. package/dist/migrate/generator/mongoCommand.d.ts +28 -0
  33. package/dist/migrate/generator/mongoCommand.js +8 -0
  34. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +2 -0
  35. package/dist/migrate/generator/mongoSchemaGenerator.js +50 -6
  36. package/dist/migrate/introspection/mongoIntrospector.js +33 -7
  37. package/dist/migrate/introspection/postgresIntrospector.js +19 -0
  38. package/dist/migrate/introspection/sqliteIntrospector.d.ts +6 -0
  39. package/dist/migrate/introspection/sqliteIntrospector.js +29 -3
  40. package/dist/mongo/mongoDialect.d.ts +2 -0
  41. package/dist/mongo/mongoDialect.js +8 -4
  42. package/dist/mongo/mongodbQuerier.js +2 -2
  43. package/dist/mssql/mssqlDialect.js +1 -0
  44. package/dist/schema/canonicalType.js +10 -4
  45. package/dist/schema/indexDifferences.d.ts +5 -2
  46. package/dist/schema/indexDifferences.js +5 -0
  47. package/dist/schema/schemaASTBuilder.js +3 -2
  48. package/dist/sqlite/localSqliteQuerierPool.d.ts +10 -11
  49. package/dist/sqlite/localSqliteQuerierPool.js +8 -11
  50. package/dist/sqlite/nodeSqliteQuerierPool.d.ts +4 -2
  51. package/dist/sqlite/nodeSqliteQuerierPool.js +7 -4
  52. package/dist/sqlite/sqliteDialect.d.ts +9 -1
  53. package/dist/sqlite/sqliteDialect.js +42 -3
  54. package/dist/sqlite/sqliteQuerierPool.d.ts +6 -5
  55. package/dist/sqlite/sqliteQuerierPool.js +10 -7
  56. package/dist/turso/tursoLocalDialect.d.ts +1 -1
  57. package/dist/turso/tursoLocalDialect.js +1 -1
  58. package/dist/turso/tursoLocalQuerierPool.d.ts +4 -5
  59. package/dist/turso/tursoLocalQuerierPool.js +3 -10
  60. package/dist/type/dialect.d.ts +5 -0
  61. package/dist/type/entity.d.ts +14 -3
  62. package/dist/type/migration.d.ts +7 -9
  63. package/dist/type/queryAggregate.d.ts +4 -8
  64. package/dist/type/vector.d.ts +17 -0
  65. package/dist/type/vector.js +6 -0
  66. package/dist/util/ddlExpression.util.d.ts +2 -0
  67. package/dist/util/ddlExpression.util.js +5 -0
  68. package/dist/util/dialect.util.d.ts +11 -1
  69. package/dist/util/dialect.util.js +21 -1
  70. package/package.json +5 -3
  71. package/skills/uql-orm/SKILL.md +142 -0
@@ -504,15 +504,26 @@ export type HookRegistration = {
504
504
  readonly methodName: string;
505
505
  };
506
506
  /**
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.
507
+ * An index type with what it needs: a vector index has to name its metric, since engines default to a
508
+ * different one than the queries use; an Atlas `vectorSearch` index may, else takes its field's, else
509
+ * cosine; a `fulltext` one may name the text-search `config` it parses with; and any other index neither.
509
510
  */
510
511
  export type IndexTypeOptions = {
511
512
  type: VectorIndexType;
512
513
  distance: VectorDistance;
514
+ config?: never;
513
515
  } | {
514
- type?: Exclude<IndexType, VectorIndexType>;
516
+ type: 'vectorSearch';
517
+ distance?: VectorDistance;
518
+ config?: never;
519
+ } | {
520
+ type: 'fulltext';
521
+ distance?: never;
522
+ config?: string;
523
+ } | {
524
+ type?: Exclude<IndexType, VectorIndexType | 'vectorSearch' | 'fulltext'>;
515
525
  distance?: never;
526
+ config?: never;
516
527
  };
517
528
  /** One index entry as the migration builder takes it: a column name, `raw`, or an object when it needs more. */
518
529
  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
@@ -122,16 +122,12 @@ type QueryGroupSchema<E, G> = Readonly<QuerySelect<E, FieldKey<E>, true>> & {
122
122
  readonly [K in Exclude<NamedKeys<G>, FieldKey<E>>]: QueryGroupRef<E>;
123
123
  } & RejectKeys<Exclude<GroupedKeys<G>, FieldKey<E>>>;
124
124
  /**
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.
125
+ * The value a path reads: the field at its end as its column holds it, `null` only where that does, since
126
+ * the path joins the rows it reads. `any` answers `unknown`: TypeScript checks a deferred type by
127
+ * instantiating it with `any`, which would walk every relation of the entity on every aggregate call.
128
128
  */
129
129
  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]>;
130
+ [K in keyof Ref & keyof E]: Ref[K] extends true ? Exclude<E[K], undefined> : GroupRefValue<RelationTarget<E[K]>, Ref[K]>;
135
131
  }[keyof Ref & keyof E];
136
132
  /** Computed columns by the alias each is read back under: `{ count: { $count: '*' }, avgAge: { $avg: { age: true } } }`. */
137
133
  export type QueryAggMap<E> = {
@@ -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
+ }
@@ -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.
@@ -114,7 +118,7 @@ export type ParsedGroupEntry<E = object> = {
114
118
  readonly alias: string;
115
119
  readonly op: QueryAggregateOp;
116
120
  readonly fieldRef: string;
117
- /** `true` for a flat distinct op (`$countDistinct`, ...) -> `COUNT(DISTINCT field)`. */
121
+ /** `true` for `$countDistinct`: `COUNT(DISTINCT field)`. */
118
122
  readonly distinct: boolean;
119
123
  /** The rows it reads, where not all of the statement's. */
120
124
  readonly where?: QueryWhere<E>;
@@ -159,6 +163,12 @@ export declare function assertNonNegativeInteger(value: number, clause: string):
159
163
  export declare function throwUnknownAggregateColumn(key: string, clause: string): never;
160
164
  /** {@link throwUnknownAggregateColumn} over every key of a clause, for backends that check up front. */
161
165
  export declare function assertAggregateColumns(clauseMap: object, emitted: ReadonlySet<string>, clause: string): void;
166
+ /** The text-search config a fulltext index builds with, which a search it serves has to parse with too. */
167
+ export declare function fulltextConfig(index: {
168
+ readonly config?: string;
169
+ }): string;
170
+ /** The fulltext index over exactly `fields`, in order, which a search of them is served by. */
171
+ export declare function fulltextIndexOver<E>(meta: EntityMeta<E>, fields: readonly string[]): EntityIndexMeta<E> | undefined;
162
172
  /**
163
173
  * The fields a `$text` searches: those it names, or else the columns of the entity's fulltext index,
164
174
  * 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
@@ -359,7 +367,7 @@ export function parseGroupMap(group, select) {
359
367
  if (key === undefined) {
360
368
  throw new TypeError(`aggregate '${alias}' names no op, only a $where`);
361
369
  }
362
- // Flat DISTINCT ops (`$countDistinct`, ...) normalize to their base op + a `distinct` flag.
370
+ // `$countDistinct` normalizes to `$count` plus a `distinct` flag.
363
371
  const { op, distinct } = resolveAggregateOp(key);
364
372
  const fieldRef = aggregateFieldRef(alias, call[key]);
365
373
  entries.push({ kind: 'fn', alias, op, fieldRef, distinct, ...(where && hasKeys(where) ? { where } : {}) });
@@ -432,6 +440,18 @@ export function assertAggregateColumns(clauseMap, emitted, clause) {
432
440
  }
433
441
  }
434
442
  }
443
+ /** The text-search config a fulltext index builds with where it states none: language-neutral, no stemming. */
444
+ const DEFAULT_TEXT_CONFIG = 'simple';
445
+ /** The text-search config a fulltext index builds with, which a search it serves has to parse with too. */
446
+ export function fulltextConfig(index) {
447
+ return index.config ?? DEFAULT_TEXT_CONFIG;
448
+ }
449
+ /** The fulltext index over exactly `fields`, in order, which a search of them is served by. */
450
+ export function fulltextIndexOver(meta, fields) {
451
+ return meta.indexes?.find((index) => index.type === 'fulltext' &&
452
+ index.columns.length === fields.length &&
453
+ index.columns.every((entry, at) => entry.column === fields[at]));
454
+ }
435
455
  /**
436
456
  * The fields a `$text` searches: those it names, or else the columns of the entity's fulltext index,
437
457
  * 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.71.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",
@@ -167,6 +168,7 @@
167
168
  "url": "https://github.com/rogerpadilla/uql/issues"
168
169
  },
169
170
  "keywords": [
171
+ "tanstack-intent",
170
172
  "orm",
171
173
  "uql",
172
174
  "sql",
@@ -0,0 +1,142 @@
1
+ ---
2
+ name: uql-orm
3
+ description: >
4
+ Write code with UQL (the uql-orm package), the TypeScript ORM whose queries are plain JSON objects,
5
+ on PostgreSQL, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, MongoDB, Turso, Neon, D1 and PGlite.
6
+ Use when a project imports uql-orm, or when defining entities, querying, populating relations,
7
+ writing transactions, raw SQL or migrations with it. UQL is not Prisma, Drizzle, TypeORM or MikroORM:
8
+ their APIs do not carry over.
9
+ ---
10
+
11
+ # UQL
12
+
13
+ Entities are classes; a query is a JSON object checked key by key against the entity; the same query runs
14
+ on every database UQL supports. There is no schema file, no generated client, and no query builder.
15
+
16
+ The full docs are Markdown at https://uql-orm.dev/llms.txt, one page per URL. Read the page for the task
17
+ before guessing an option: every page listed there is a `.md` URL.
18
+
19
+ ## Setup
20
+
21
+ ```sh
22
+ npm install uql-orm pg # or mysql2, mariadb, better-sqlite3, mongodb, @libsql/client, ...
23
+ ```
24
+
25
+ ESM only. Node 24+, Bun, Deno or an edge runtime, TypeScript 5.2+. Decorators are the TC39 standard:
26
+ never enable `experimentalDecorators` or `emitDecoratorMetadata`, never import `reflect-metadata`.
27
+ In `tsconfig.json`, `module` is `nodenext` or `preserve`, and `target` is a dated one (`es2022`+), not `esnext`.
28
+
29
+ ```ts
30
+ // uql.config.ts
31
+ import type { Config } from 'uql-orm';
32
+ import { PgQuerierPool } from 'uql-orm/postgres';
33
+ import { Post, User } from './entities.js';
34
+
35
+ export const pool = new PgQuerierPool({ connectionString: process.env.DATABASE_URL });
36
+
37
+ export default { pool, entities: [User, Post] } satisfies Config;
38
+ ```
39
+
40
+ Each driver has its own entry point: `uql-orm/postgres`, `uql-orm/mysql`, `uql-orm/maria`, `uql-orm/sqlite`,
41
+ `uql-orm/mongo`, `uql-orm/libsql`, `uql-orm/turso`, `uql-orm/neon`, `uql-orm/d1`, `uql-orm/pglite`,
42
+ `uql-orm/bunSql`, `uql-orm/mssql`, `uql-orm/cockroachdb`. Build the pool once per process and import it.
43
+
44
+ ## Entities
45
+
46
+ ```ts
47
+ import { Entity, Field, Id, ManyToOne, OneToMany } from 'uql-orm';
48
+
49
+ @Entity()
50
+ export class User {
51
+ @Id({ type: 'uuid', onInsert: () => crypto.randomUUID() })
52
+ id?: string;
53
+
54
+ @Field({ type: String, unique: true, nullable: false })
55
+ email?: string;
56
+
57
+ @OneToMany({ entity: () => Post, mappedBy: (post) => post.author })
58
+ posts?: Post[];
59
+ }
60
+
61
+ @Entity()
62
+ export class Post {
63
+ @Id({ type: Number })
64
+ id?: number;
65
+
66
+ @Field({ type: String })
67
+ title?: string | null;
68
+
69
+ @Field({ references: () => User })
70
+ authorId?: string | null;
71
+
72
+ @ManyToOne({ entity: () => User, references: (post) => post.authorId })
73
+ author?: User;
74
+ }
75
+ ```
76
+
77
+ - Every `@Field` states its `type` (`String`, `Number`, `Boolean`, `Date`, `BigInt`, or a column type such as
78
+ `'uuid'`, `'text'`, `'jsonb'`), except a foreign key, which takes `references` and inherits the target key's type.
79
+ - A column is nullable unless it says `nullable: false`, and its property must admit `null` to match:
80
+ `title?: string | null`. A property typed without `| null` on a nullable column is a compile error.
81
+ - Members are named by callbacks, never by strings: `mappedBy: (post) => post.author`, `references: (post) => post.authorId`.
82
+ - `@ManyToMany({ entity: () => Tag, through: () => PostTag })` names its junction entity.
83
+ - `defineEntity` defines the same entity without decorators: https://uql-orm.dev/entities/imperative.md
84
+
85
+ ## Queries
86
+
87
+ ```ts
88
+ import { pool } from './uql.config.js';
89
+ import { User } from './entities.js';
90
+
91
+ const users = await pool.findMany(User, {
92
+ $select: { id: true, email: true },
93
+ $where: { email: { $endsWith: '@uql-orm.dev' }, $or: [{ id: 'a' }, { id: 'b' }] },
94
+ $populate: { posts: { $select: { title: true }, $sort: { id: 'desc' }, $limit: 5 } },
95
+ $sort: { email: 'asc' },
96
+ $skip: 0,
97
+ $limit: 20,
98
+ });
99
+ ```
100
+
101
+ - The keys are `$select`, `$exclude`, `$where`, `$populate`, `$sort`, `$skip`, `$limit`.
102
+ - `$where` takes a value for equality or an operator map: `$eq`, `$ne`, `$lt`, `$lte`, `$gt`, `$gte`, `$in`,
103
+ `$nin`, `$between`, `$like`, `$ilike`, `$regex`, `$startsWith`, `$endsWith`, `$includes`, `$isNull`,
104
+ `$isNotNull`. `$and`, `$or`, `$not` and `$nor` combine clauses.
105
+ - A result is narrowed to what the query selected and populated: reading an unselected field is a compile error.
106
+ Name that shape with `QueryFindResult<User, 'id' | 'email'>` rather than widening the query.
107
+ - `$populate` loads relations in the same statement. Nothing is lazy: a relation not populated is not there.
108
+ - A query is plain data, so it can be built dynamically, stored, or sent from a browser to `uql-orm/http`.
109
+ - Methods: `findMany`, `findOne`, `findOneById`, `findManyAndCount`, `findManyStream`, `count`, `exists`,
110
+ `aggregate`, `insertOne`, `insertMany`, `updateOneById`, `updateMany`, `saveOne`, `saveMany`, `upsertOne`,
111
+ `upsertMany`, `deleteOneById`, `deleteMany`. Each takes the entity class first.
112
+ - `updateMany` and `deleteMany` with no `$where` throw; `{ unfiltered: true }` means the whole table.
113
+ - `raw()` embeds SQL anywhere a value or field goes; `pool.all(sql, values)` runs a raw `SELECT`.
114
+
115
+ ## Connections and transactions
116
+
117
+ Every method is on both the pool and a querier. A pool call acquires a connection for that call and releases it.
118
+ To run several operations on one connection, or atomically, hold a querier:
119
+
120
+ ```ts
121
+ await pool.transaction(async (querier) => {
122
+ const userId = await querier.insertOne(User, { email: 'ada@uql-orm.dev' });
123
+ await querier.insertOne(Post, { title: 'Hello', authorId: userId });
124
+ });
125
+ ```
126
+
127
+ Inside the callback, call `querier`, never `pool`: a `pool` call runs on another connection, outside the
128
+ transaction. A querier from `pool.getQuerier()` is yours to release: bind it with `await using`.
129
+
130
+ ## Migrations
131
+
132
+ `npx uql-migrate` reads `uql.config.ts`. `sync` creates what the entities imply (development only);
133
+ `generate:entities` writes the diff as a migration file to review; `up` applies migrations; `generate:from-db`
134
+ writes entity classes from an existing database; `drift:check` fails when the database no longer matches.
135
+
136
+ ## Where to read more
137
+
138
+ - Operators, per-dialect SQL: https://uql-orm.dev/querying/comparison-operators.md
139
+ - Relations and deep `$populate`: https://uql-orm.dev/querying/relations.md
140
+ - Every method's signature: https://uql-orm.dev/querying/methods.md
141
+ - Coming from Prisma, Drizzle, TypeORM or MikroORM: https://uql-orm.dev/switching-to-uql.md
142
+ - Breaking changes by version: https://uql-orm.dev/upgrade-guide.md