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/dist/type/entity.d.ts
CHANGED
|
@@ -504,15 +504,26 @@ export type HookRegistration = {
|
|
|
504
504
|
readonly methodName: string;
|
|
505
505
|
};
|
|
506
506
|
/**
|
|
507
|
-
* An index type with
|
|
508
|
-
*
|
|
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
|
|
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;
|
package/dist/type/migration.d.ts
CHANGED
|
@@ -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
|
|
126
|
-
* `any` answers `unknown`: TypeScript checks a deferred type by
|
|
127
|
-
* walk every relation of the entity on every aggregate call
|
|
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] :
|
|
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> = {
|
package/dist/type/vector.d.ts
CHANGED
|
@@ -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.
|
package/dist/type/vector.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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.
|
|
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
|