uql-orm 0.71.0 → 0.72.1
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 +1 -1
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +3 -3
- package/dist/dialect/abstractSqlDialect.d.ts +15 -2
- package/dist/dialect/abstractSqlDialect.js +60 -11
- package/dist/dialect/aliases.d.ts +2 -0
- package/dist/dialect/aliases.js +2 -0
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +5 -0
- package/dist/dialect/mysqlLikeSqlDialect.js +9 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +7 -0
- package/dist/dialect/pgLikeSqlDialect.js +24 -5
- package/dist/dialect/queryJoins.d.ts +0 -1
- package/dist/entity/metadata/definition.js +4 -2
- package/dist/maria/mariaDialect.js +1 -2
- package/dist/migrate/cli.js +2 -1
- package/dist/migrate/ddl/indexDdl.d.ts +2 -0
- package/dist/migrate/ddl/indexDdl.js +4 -0
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +5 -0
- package/dist/migrate/ddl/mysqlIndexDdl.js +7 -0
- package/dist/migrate/generator/mongoCommand.d.ts +2 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +3 -0
- package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
- package/dist/migrate/introspection/mongoIntrospector.js +12 -6
- package/dist/migrate/schemaGenerator.d.ts +4 -1
- package/dist/migrate/schemaGenerator.js +14 -7
- package/dist/mongo/mongoDialect.d.ts +3 -3
- package/dist/mongo/mongoDialect.js +32 -13
- package/dist/mongo/mongodbQuerier.js +2 -3
- package/dist/mssql/mssqlDialect.js +1 -0
- package/dist/schema/indexDifferences.d.ts +2 -1
- package/dist/schema/indexDifferences.js +8 -1
- package/dist/schema/schemaASTBuilder.d.ts +2 -0
- package/dist/schema/schemaASTBuilder.js +23 -0
- package/dist/sqlite/sqliteDialect.d.ts +2 -0
- package/dist/sqlite/sqliteDialect.js +5 -0
- package/dist/type/dialect.d.ts +5 -0
- package/dist/type/entity.d.ts +18 -10
- package/dist/type/query.d.ts +11 -4
- package/dist/type/queryAggregate.d.ts +1 -8
- package/dist/type/utility.d.ts +13 -0
- package/dist/util/dialect.util.d.ts +29 -1
- package/dist/util/dialect.util.js +50 -1
- package/package.json +1 -2
- package/skills/uql-orm/SKILL.md +6 -2
|
@@ -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.
|
package/dist/type/utility.d.ts
CHANGED
|
@@ -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,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { IndexType } from '../schema/types.js';
|
|
2
|
+
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
3
|
export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
|
|
3
4
|
/** The keys of `payload` a write persists as columns. */
|
|
4
5
|
export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E> | UpdatePayload<E>, callbackKey: CallbackKey): FieldKey<E>[];
|
|
@@ -87,6 +88,10 @@ export declare function findVectorIndex<E>(meta: EntityMeta<E>, key: string): En
|
|
|
87
88
|
export declare function hasVectorNear(where: unknown): boolean;
|
|
88
89
|
/** Type guard: checks whether an update payload value is a JSON operator object. */
|
|
89
90
|
export declare function isJsonUpdateOp(value: unknown): value is JsonUpdateOp;
|
|
91
|
+
/** Type guard: checks whether an update payload value is a scalar field's operator. */
|
|
92
|
+
export declare function isFieldUpdateOp(value: unknown): value is FieldUpdateOp;
|
|
93
|
+
/** The one operator a scalar field's update carries, and its operand. */
|
|
94
|
+
export declare function fieldUpdateOf(value: FieldUpdateOp): [keyof FieldUpdateOp, number | bigint];
|
|
90
95
|
/**
|
|
91
96
|
* The `$where` naming rows by key: a bare value names the one key column (refused on a composite), a
|
|
92
97
|
* composite's key map is a `$where` already, and a list is an `IN` of bare values or an OR of maps.
|
|
@@ -167,8 +172,31 @@ export declare function assertAggregateColumns(clauseMap: object, emitted: Reado
|
|
|
167
172
|
export declare function fulltextConfig(index: {
|
|
168
173
|
readonly config?: string;
|
|
169
174
|
}): string;
|
|
175
|
+
/**
|
|
176
|
+
* Each column's weight in a fulltext index, 1 where it states none, or none at all where they are alike.
|
|
177
|
+
* Checked wherever it is read, so the migration, the search and its rank refuse the same declaration.
|
|
178
|
+
*/
|
|
179
|
+
export declare function fulltextWeights(index: {
|
|
180
|
+
readonly type?: IndexType;
|
|
181
|
+
readonly entries: readonly {
|
|
182
|
+
readonly weight?: number;
|
|
183
|
+
}[];
|
|
184
|
+
}): readonly number[] | undefined;
|
|
185
|
+
/**
|
|
186
|
+
* A weighted fulltext index's lightest weight, and what each column weighs beyond it: the score over every
|
|
187
|
+
* column counts the lightest, and a column weighing more adds its own score times the rest.
|
|
188
|
+
*/
|
|
189
|
+
export declare function textWeightSteps(weights: readonly number[]): {
|
|
190
|
+
lightest: number;
|
|
191
|
+
extra: number[];
|
|
192
|
+
};
|
|
170
193
|
/** The fulltext index over exactly `fields`, in order, which a search of them is served by. */
|
|
171
194
|
export declare function fulltextIndexOver<E>(meta: EntityMeta<E>, fields: readonly string[]): EntityIndexMeta<E> | undefined;
|
|
195
|
+
/**
|
|
196
|
+
* The search a `$sort` by `$text` ranks by: the one at the root of the same query's `$where`. A nested or
|
|
197
|
+
* negated one has no score to order by, and MongoDB scores only the one `$text` it allows.
|
|
198
|
+
*/
|
|
199
|
+
export declare function rankedTextSearch<E>(where: QueryWhere<E> | undefined): QueryTextSearchOptions<E>;
|
|
172
200
|
/**
|
|
173
201
|
* The fields a `$text` searches: those it names, or else the columns of the entity's fulltext index,
|
|
174
202
|
* the declaration MySQL's `MATCH` has to name exactly and a MongoDB text index already is. Refused
|
|
@@ -236,6 +236,16 @@ const JSON_UPDATE_OPS = [
|
|
|
236
236
|
export function isJsonUpdateOp(value) {
|
|
237
237
|
return isRecord(value) && someKey(value, (key) => JSON_UPDATE_OPS.includes(key));
|
|
238
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
|
+
}
|
|
239
249
|
/**
|
|
240
250
|
* The `$where` naming rows by key: a bare value names the one key column (refused on a composite), a
|
|
241
251
|
* composite's key map is a `$where` already, and a list is an `IN` of bare values or an OR of maps.
|
|
@@ -370,7 +380,7 @@ export function parseGroupMap(group, select) {
|
|
|
370
380
|
// `$countDistinct` normalizes to `$count` plus a `distinct` flag.
|
|
371
381
|
const { op, distinct } = resolveAggregateOp(key);
|
|
372
382
|
const fieldRef = aggregateFieldRef(alias, call[key]);
|
|
373
|
-
entries.push({ kind: 'fn', alias, op, fieldRef, distinct, ...(
|
|
383
|
+
entries.push({ kind: 'fn', alias, op, fieldRef, distinct, ...(hasKeys(where) ? { where } : {}) });
|
|
374
384
|
}
|
|
375
385
|
return entries;
|
|
376
386
|
}
|
|
@@ -446,12 +456,51 @@ const DEFAULT_TEXT_CONFIG = 'simple';
|
|
|
446
456
|
export function fulltextConfig(index) {
|
|
447
457
|
return index.config ?? DEFAULT_TEXT_CONFIG;
|
|
448
458
|
}
|
|
459
|
+
/** The largest weight MongoDB's text index takes, which truncates a fraction to the whole number below. */
|
|
460
|
+
const MAX_TEXT_WEIGHT = 99_999;
|
|
461
|
+
/**
|
|
462
|
+
* Each column's weight in a fulltext index, 1 where it states none, or none at all where they are alike.
|
|
463
|
+
* Checked wherever it is read, so the migration, the search and its rank refuse the same declaration.
|
|
464
|
+
*/
|
|
465
|
+
export function fulltextWeights(index) {
|
|
466
|
+
if (!index.entries.some((entry) => entry.weight !== undefined)) {
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
if (index.type !== 'fulltext') {
|
|
470
|
+
throw new TypeError(`a column weight ranks a fulltext index, and this one is ${index.type ?? 'btree'}`);
|
|
471
|
+
}
|
|
472
|
+
const weights = index.entries.map(({ weight = 1 }) => {
|
|
473
|
+
if (!Number.isInteger(weight) || weight < 1 || weight > MAX_TEXT_WEIGHT) {
|
|
474
|
+
throw new TypeError(`a column weight is a whole number from 1 to ${MAX_TEXT_WEIGHT}, not ${weight}`);
|
|
475
|
+
}
|
|
476
|
+
return weight;
|
|
477
|
+
});
|
|
478
|
+
return new Set(weights).size > 1 ? weights : undefined;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* A weighted fulltext index's lightest weight, and what each column weighs beyond it: the score over every
|
|
482
|
+
* column counts the lightest, and a column weighing more adds its own score times the rest.
|
|
483
|
+
*/
|
|
484
|
+
export function textWeightSteps(weights) {
|
|
485
|
+
const lightest = Math.min(...weights);
|
|
486
|
+
return { lightest, extra: weights.map((weight) => weight - lightest) };
|
|
487
|
+
}
|
|
449
488
|
/** The fulltext index over exactly `fields`, in order, which a search of them is served by. */
|
|
450
489
|
export function fulltextIndexOver(meta, fields) {
|
|
451
490
|
return meta.indexes?.find((index) => index.type === 'fulltext' &&
|
|
452
491
|
index.columns.length === fields.length &&
|
|
453
492
|
index.columns.every((entry, at) => entry.column === fields[at]));
|
|
454
493
|
}
|
|
494
|
+
/**
|
|
495
|
+
* The search a `$sort` by `$text` ranks by: the one at the root of the same query's `$where`. A nested or
|
|
496
|
+
* negated one has no score to order by, and MongoDB scores only the one `$text` it allows.
|
|
497
|
+
*/
|
|
498
|
+
export function rankedTextSearch(where) {
|
|
499
|
+
if (!where?.$text) {
|
|
500
|
+
throw new TypeError('$sort by $text ranks by the $text at the root of $where, which this query has none of');
|
|
501
|
+
}
|
|
502
|
+
return where.$text;
|
|
503
|
+
}
|
|
455
504
|
/**
|
|
456
505
|
* The fields a `$text` searches: those it names, or else the columns of the entity's fulltext index,
|
|
457
506
|
* 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.72.1",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|
|
@@ -168,7 +168,6 @@
|
|
|
168
168
|
"url": "https://github.com/rogerpadilla/uql/issues"
|
|
169
169
|
},
|
|
170
170
|
"keywords": [
|
|
171
|
-
"tanstack-intent",
|
|
172
171
|
"orm",
|
|
173
172
|
"uql",
|
|
174
173
|
"sql",
|
package/skills/uql-orm/SKILL.md
CHANGED
|
@@ -98,7 +98,8 @@ const users = await pool.findMany(User, {
|
|
|
98
98
|
});
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
- The keys are `$select`, `$exclude`, `$where`, `$populate`, `$sort`, `$skip`, `$limit
|
|
101
|
+
- The keys are `$select`, `$exclude`, `$where`, `$populate`, `$count`, `$distinct`, `$sort`, `$skip`, `$limit`;
|
|
102
|
+
`$count: { posts: true }` tallies a to-many under `_count` without loading it.
|
|
102
103
|
- `$where` takes a value for equality or an operator map: `$eq`, `$ne`, `$lt`, `$lte`, `$gt`, `$gte`, `$in`,
|
|
103
104
|
`$nin`, `$between`, `$like`, `$ilike`, `$regex`, `$startsWith`, `$endsWith`, `$includes`, `$isNull`,
|
|
104
105
|
`$isNotNull`. `$and`, `$or`, `$not` and `$nor` combine clauses.
|
|
@@ -109,7 +110,10 @@ const users = await pool.findMany(User, {
|
|
|
109
110
|
- Methods: `findMany`, `findOne`, `findOneById`, `findManyAndCount`, `findManyStream`, `count`, `exists`,
|
|
110
111
|
`aggregate`, `insertOne`, `insertMany`, `updateOneById`, `updateMany`, `saveOne`, `saveMany`, `upsertOne`,
|
|
111
112
|
`upsertMany`, `deleteOneById`, `deleteMany`. Each takes the entity class first.
|
|
112
|
-
- `updateMany` and `deleteMany`
|
|
113
|
+
- `updateMany` and `deleteMany` naming no rows - no `$where`, no `$limit` - throw; `{ unfiltered: true }` means the whole table.
|
|
114
|
+
- An update takes `{ stock: { $inc: -1 } }` to add, or `$mul` to multiply, in the statement, a NULL counting as 0,
|
|
115
|
+
so a guard in `$where` (`stock: { $gte: 1 }`) makes a decrement race-safe. JSON fields take `$set`, `$unset`,
|
|
116
|
+
`$push`, `$pull`.
|
|
113
117
|
- `raw()` embeds SQL anywhere a value or field goes; `pool.all(sql, values)` runs a raw `SELECT`.
|
|
114
118
|
|
|
115
119
|
## Connections and transactions
|