uql-orm 0.71.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.
package/README.md CHANGED
@@ -65,7 +65,7 @@ The compiler catches each of those, with no codegen: the entity classes are the
65
65
  - [Entities](https://uql-orm.dev/entities/basic) - decorators, relations, hooks, or the decorator-free [imperative API](https://uql-orm.dev/entities/imperative)
66
66
  - [Switching to UQL](https://uql-orm.dev/switching-to-uql) - coming from Prisma, Drizzle, TypeORM, or MikroORM
67
67
 
68
- Using a coding agent? The package ships a [skill](skills/uql-orm/SKILL.md) for it (`npx @tanstack/intent install`, or `npx skills add rogerpadilla/uql`), and every docs page is Markdown, indexed at [uql-orm.dev/llms.txt](https://uql-orm.dev/llms.txt).
68
+ Using a coding agent? The package ships a [skill](skills/uql-orm/SKILL.md) for it, and every docs page is Markdown: [set it up](https://uql-orm.dev/ai-agents).
69
69
 
70
70
  Release notes live in [CHANGELOG.md](https://github.com/rogerpadilla/uql/blob/main/CHANGELOG.md).
71
71
 
@@ -1,4 +1,4 @@
1
- var p=[];function y(e){for(let n of p)n(e)}function _(e){p.push(e);let n=p.length-1;return()=>{p.splice(n,1)}}var S=["$select","$populate","$exclude","$where","$sort"],E=["$count"],O=["$skip","$limit"],f=["$candidates"],T=["$distinct"],A=["$lock",...E,...f];var b=Symbol("rawValue"),W=Symbol("rawAlias");function c(e){return e?Object.keys(e):[]}function C(e){if(typeof e!=="object"||e===null)return!0;if(Array.isArray(e))return!1;let n=Object.getPrototypeOf(e);return n!==Object.prototype&&n!==null}var Y=new Set([...S,...E,...O,...f,...T,"hardDelete","count"]);function i(e){if(!e)return"";let n=new URLSearchParams;for(let r of c(e)){let o=e[r];if(o===void 0)continue;n.append(r,typeof o==="object"&&o!==null?u(o):String(o))}let t=n.toString();return t?`?${t}`:""}function u(e){return JSON.stringify(e,(n,t)=>{if(typeof t!=="object"||t===null)return t;if(b in t)throw TypeError("raw SQL cannot travel over HTTP: what leaves the browser is JSON");if(t instanceof ArrayBuffer||ArrayBuffer.isView(t))throw TypeError("binary cannot travel over HTTP: what leaves the browser is JSON");return t})}class k extends Error{status;constructor(e,n){super(e);this.status=n;this.name="RequestError"}}function R(e,n){return a(e,{method:"get"},n)}function h(e,n,t){return a(e,{method:"post",body:u(n)},t)}function x(e,n,t){return a(e,{method:"patch",body:u(n)},t)}function Q(e,n,t){return a(e,{method:"put",body:u(n)},t)}function g(e,n){return a(e,{method:"delete"},n)}function K(e,n,t){return a(e,{method:"QUERY",body:u(n)},t)}function a(e,n,t){if(y({phase:"start",opts:t}),n.headers={accept:"application/json","content-type":"application/json",...t?.headers},t?.signal)n.signal=t.signal;return fetch(e,n).then((r)=>r.json().then((o)=>{if(r.status>=200&&r.status<300)return y({phase:"success",opts:t}),o;let P=o,l={message:P?.error?.message??r.statusText,code:P?.error?.code??r.status};throw y({phase:"error",error:l,opts:t}),new k(l.message,l.code)})).finally(()=>{y({phase:"complete",opts:t})})}function U(e){let n=e.charAt(0).toLowerCase();for(let t=1;t<e.length;++t)n+=e[t]===e[t].toUpperCase()?"-"+e[t].toLowerCase():e[t];return n}var s={findMany:{method:"GET",path:""},findOne:{method:"GET",path:"/one"},count:{method:"GET",path:"/count"},findOneById:{method:"GET",path:"/:id"},insertOne:{method:"POST",path:""},insertMany:{method:"POST",path:"/many"},saveOne:{method:"PUT",path:""},saveMany:{method:"PUT",path:"/many"},updateMany:{method:"PATCH",path:""},updateOneById:{method:"PATCH",path:"/:id"},deleteOneById:{method:"DELETE",path:"/:id"},deleteMany:{method:"DELETE",path:""}},V=c(s),ne=new Map(V.filter((e)=>s[e].method==="GET"&&s[e].path!=="/:id").map((e)=>[s[e].path,e]));function j(e){return U(e.name)}function w(e,n){if(!C(n))throw TypeError(`'${e.name}' was addressed by an id object, which the HTTP route cannot carry.`);return String(n)}class m{basePath;defaults;constructor(e,n={}){this.basePath=e;this.defaults=n}async findOneById(e,n,t,r){let o=this.getBasePath(e),d=i(t);return R(`${o}/${w(e,n)}${d}`,this.buildOptions(r))}findOne(e,n,t){return this.read(`${this.getBasePath(e)}${s.findOne.path}`,n,t)}findMany(e,n,t){let r={...n};if(t?.count)r.count=!0;return this.read(this.getBasePath(e),r,t)}async findManyAndCount(e,n,t){let r=await this.findMany(e,n,{...t,count:!0});if(typeof r.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...r,count:r.count}}count(e,n,t){return this.read(`${this.getBasePath(e)}${s.count.path}`,n,t)}async exists(e,n,t){let r=await this.count(e,{...n,$limit:1},t);return{...r,data:r.data>0}}insertOne(e,n,t){let r=this.getBasePath(e);return h(r,n,this.buildOptions(t))}insertMany(e,n,t){let r=this.getBasePath(e);return h(`${r}${s.insertMany.path}`,n,this.buildOptions(t))}async updateOneById(e,n,t,r){let o=this.getBasePath(e);return x(`${o}/${w(e,n)}`,t,this.buildOptions(r))}updateMany(e,n,t,r){let o=this.getBasePath(e),d=i(n);return x(`${o}${d}`,t,this.buildOptions(r))}saveOne(e,n,t){let r=this.getBasePath(e);return Q(r,n,this.buildOptions(t))}saveMany(e,n,t){let r=this.getBasePath(e);return Q(`${r}${s.saveMany.path}`,n,this.buildOptions(t))}async deleteOneById(e,n,t={}){let r=this.getBasePath(e),o=t.hardDelete?i({hardDelete:t.hardDelete}):"";return g(`${r}/${w(e,n)}${o}`,this.buildOptions(t))}deleteMany(e,n,t={}){let r=this.getBasePath(e),o=i(t.hardDelete?{...n,hardDelete:t.hardDelete}:n);return g(`${r}${o}`,this.buildOptions(t))}getBasePath(e){return`${this.basePath}/${(this.defaults.entityPath??j)(e)}`}read(e,n,t){if(this.defaults.readMethod==="QUERY")return K(e,n??{},this.buildOptions(t));return R(`${e}${i(n)}`,this.buildOptions(t))}buildOptions(e){if(!this.defaults.headers&&!e?.headers)return e;return{...e,headers:{...this.defaults.headers,...e?.headers}}}}var q={getQuerier:()=>new m("/api")};function de(e){q=e}function F(){return q}function pe(){return F().getQuerier()}export{m as HttpQuerier,k as RequestError,R as get,pe as getQuerier,F as getQuerierPool,y as notify,_ as on,x as patch,h as post,Q as put,K as query,g as remove,de as setQuerierPool};
1
+ var p=[];function y(e){for(let n of p)n(e)}function _(e){p.push(e);let n=p.length-1;return()=>{p.splice(n,1)}}var P=["$select","$populate","$exclude","$where","$sort"],E=["$count"],O=["$skip","$limit"],f=["$candidates"],T=["$distinct"],A=["$lock",...E,...f];var b=Symbol("rawValue"),W=Symbol("rawAlias");function c(e){return e?Object.keys(e):[]}function C(e){if(typeof e!=="object"||e===null)return!0;if(Array.isArray(e))return!1;let n=Object.getPrototypeOf(e);return n!==Object.prototype&&n!==null}var Y=new Set([...P,...E,...O,...f,...T,"hardDelete","count"]);function i(e){if(!e)return"";let n=new URLSearchParams;for(let r of c(e)){let o=e[r];if(o===void 0)continue;n.append(r,typeof o==="object"&&o!==null?u(o):String(o))}let t=n.toString();return t?`?${t}`:""}function u(e){return JSON.stringify(e,(n,t)=>{if(typeof t!=="object"||t===null)return t;if(b in t)throw TypeError("raw SQL cannot travel over HTTP: what leaves the browser is JSON");if(t instanceof ArrayBuffer||ArrayBuffer.isView(t))throw TypeError("binary cannot travel over HTTP: what leaves the browser is JSON");return t})}class k extends Error{status;constructor(e,n){super(e);this.status=n;this.name="RequestError"}}function R(e,n){return a(e,{method:"get"},n)}function h(e,n,t){return a(e,{method:"post",body:u(n)},t)}function x(e,n,t){return a(e,{method:"patch",body:u(n)},t)}function Q(e,n,t){return a(e,{method:"put",body:u(n)},t)}function g(e,n){return a(e,{method:"delete"},n)}function K(e,n,t){return a(e,{method:"QUERY",body:u(n)},t)}function a(e,n,t){if(y({phase:"start",opts:t}),n.headers={accept:"application/json","content-type":"application/json",...t?.headers},t?.signal)n.signal=t.signal;return fetch(e,n).then((r)=>r.json().then((o)=>{if(r.status>=200&&r.status<300)return y({phase:"success",opts:t}),o;let S=o,l={message:S?.error?.message??r.statusText,code:S?.error?.code??r.status};throw y({phase:"error",error:l,opts:t}),new k(l.message,l.code)})).finally(()=>{y({phase:"complete",opts:t})})}function U(e){let n=e.charAt(0).toLowerCase();for(let t=1;t<e.length;++t)n+=e[t]===e[t].toUpperCase()?"-"+e[t].toLowerCase():e[t];return n}var s={findMany:{method:"GET",path:""},findOne:{method:"GET",path:"/one"},count:{method:"GET",path:"/count"},findOneById:{method:"GET",path:"/:id"},insertOne:{method:"POST",path:""},insertMany:{method:"POST",path:"/many"},saveOne:{method:"PUT",path:""},saveMany:{method:"PUT",path:"/many"},updateMany:{method:"PATCH",path:""},updateOneById:{method:"PATCH",path:"/:id"},deleteOneById:{method:"DELETE",path:"/:id"},deleteMany:{method:"DELETE",path:""}},V=c(s),ne=new Map(V.filter((e)=>s[e].method==="GET"&&s[e].path!=="/:id").map((e)=>[s[e].path,e]));function j(e){return U(e.name)}function w(e,n){if(!C(n))throw TypeError(`'${e.name}' was addressed by an id object, which the HTTP route cannot carry.`);return String(n)}class m{basePath;defaults;constructor(e,n={}){this.basePath=e;this.defaults=n}async findOneById(e,n,t,r){let o=this.getBasePath(e),d=i(t);return R(`${o}/${w(e,n)}${d}`,this.buildOptions(r))}findOne(e,n,t){return this.read(`${this.getBasePath(e)}${s.findOne.path}`,n,t)}findMany(e,n,t){let r={...n};if(t?.count)r.count=!0;return this.read(this.getBasePath(e),r,t)}async findManyAndCount(e,n,t){let r=await this.findMany(e,n,{...t,count:!0});if(typeof r.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...r,count:r.count}}count(e,n,t){return this.read(`${this.getBasePath(e)}${s.count.path}`,n,t)}async exists(e,n,t){let r=await this.count(e,{...n,$limit:1},t);return{...r,data:r.data>0}}insertOne(e,n,t){let r=this.getBasePath(e);return h(r,n,this.buildOptions(t))}insertMany(e,n,t){let r=this.getBasePath(e);return h(`${r}${s.insertMany.path}`,n,this.buildOptions(t))}async updateOneById(e,n,t,r){let o=this.getBasePath(e);return x(`${o}/${w(e,n)}`,t,this.buildOptions(r))}updateMany(e,n,t,r){let o=this.getBasePath(e),d=i(n);return x(`${o}${d}`,t,this.buildOptions(r))}saveOne(e,n,t){let r=this.getBasePath(e);return Q(r,n,this.buildOptions(t))}saveMany(e,n,t){let r=this.getBasePath(e);return Q(`${r}${s.saveMany.path}`,n,this.buildOptions(t))}async deleteOneById(e,n,t={}){let r=this.getBasePath(e),o=t.hardDelete?i({hardDelete:t.hardDelete}):"";return g(`${r}/${w(e,n)}${o}`,this.buildOptions(t))}deleteMany(e,n,t={}){let r=this.getBasePath(e),o=i(t.hardDelete?{...n,hardDelete:t.hardDelete}:n);return g(`${r}${o}`,this.buildOptions(t))}getBasePath(e){return`${this.basePath}/${(this.defaults.entityPath??j)(e)}`}read(e,n,t){if(this.defaults.readMethod==="QUERY")return K(e,n??{},this.buildOptions(t));return R(`${e}${i(n)}`,this.buildOptions(t))}buildOptions(e){if(!this.defaults.headers&&!e?.headers)return e;return{...e,headers:{...this.defaults.headers,...e?.headers}}}}var q={getQuerier:()=>new m("/api")};function de(e){q=e}function F(){return q}function pe(){return F().getQuerier()}export{m as HttpQuerier,k as RequestError,R as get,pe as getQuerier,F as getQuerierPool,y as notify,_ as on,x as patch,h as post,Q as put,K as query,g as remove,de as setQuerierPool};
2
2
 
3
- //# debugId=B4B64AF2FFDB750964756E2164756E21
3
+ //# debugId=948F02FA588B6B3064756E2164756E21
4
4
  //# sourceMappingURL=uql-browser.min.js.map
@@ -3,7 +3,7 @@
3
3
  "sources": ["../../src/browser/http/bus.ts", "../../src/type/query.ts", "../../src/type/queryRaw.ts", "../../src/util/object.util.ts", "../../src/http/query.ts", "../../src/browser/http/http.ts", "../../src/util/string.util.ts", "../../src/http/contract.ts", "../../src/browser/querier/httpQuerier.ts", "../../src/browser/options.ts"],
4
4
  "sourcesContent": [
5
5
  "import type { RequestCallback, RequestNotification } from '../type/index.js';\n\nconst subscriptors: RequestCallback[] = [];\n\nexport function notify(notification: RequestNotification): void {\n for (const subscriptor of subscriptors) {\n subscriptor(notification);\n }\n}\n\nexport function on(cb: RequestCallback): () => void {\n subscriptors.push(cb);\n const index = subscriptors.length - 1;\n return (): void => {\n subscriptors.splice(index, 1);\n };\n}\n",
6
- "import type { FieldKey, JsonFieldPaths, RelationKey, RelationTarget, ToManyRelationKey, WrittenId } from './entity.js';\nimport type { QueryLock } from './queryLock.js';\nimport type { QueryRaw } from './queryRaw.js';\nimport type { QueryWhere } from './queryWhere.js';\nimport type { BooleanLike, Except, IsMany, PrimaryKey } from './utility.js';\nimport type { QueryVectorSearch } from './vector.js';\n\nexport type QueryOptions = {\n /**\n * Toggle named entity filters for this query. `false` disables all filters;\n * `{ softDelete: false }` disables one; `{ myFilter: true }` force-enables a `default: false` filter.\n * Security filters cannot be disabled here.\n */\n filters?: false | Record<string, boolean>;\n /**\n * Delete only: physically remove rows instead of soft-deleting, ignoring the soft-delete filter so\n * already-deleted rows are removed too. No effect on entities without a soft-delete field.\n */\n hardDelete?: boolean;\n /**\n * `updateMany`/`deleteMany` only: address every row of the table on purpose. Without it a bulk write\n * that names none - no `$where` and no `$limit` - is refused, since a forgotten filter and the whole\n * table look alike. The entity's own filters never count as naming one.\n */\n unfiltered?: boolean;\n /**\n * prefix the query with this.\n */\n prefix?: string;\n /**\n * automatically infer the prefix for the query.\n */\n autoPrefix?: boolean;\n};\n\n/**\n * Field selection - `{ name: true }` whitelists fields; relations go in `$populate`. Declared over\n * `F extends keyof E`, like every map keyed by an entity's members, so each key stays linked to its\n * property and an editor rename reaches it. `F` is also how a projection passes its captured key set.\n */\nexport type QuerySelect<E, F extends keyof E = FieldKey<E>, V = BooleanLike> = {\n [K in F]?: V;\n};\n\n/**\n * Accepted `$select` value: a field map, or raw SQL projections built with `raw()`\n * (e.g. ``[raw`*`, raw`LOG10(points)`.as('score')]``). The raw form is SQL-only.\n */\nexport type QuerySelectValue<E, Raw = QueryRaw> = QuerySelect<E> | readonly Raw[];\n\n/**\n * Fields to exclude from the query result - `{ name: true }` blacklists fields.\n * Mutually exclusive with positive field selections in `$select`.\n */\nexport type QueryExclude<E> = QuerySelect<E>;\n\n/**\n * relation population map.\n */\nexport type QueryPopulate<E, Raw = QueryRaw, R extends keyof E = RelationKey<E>> = {\n [K in R]?: BooleanLike | QueryPopulateRelationOptions<E[K], Raw>;\n};\n\n/**\n * The key a read carries its relation tallies under. One spelling for the type and the runtime that\n * fills it: they sit in different modules, so a drift would type-check and answer `undefined`.\n */\nexport const COUNT_RESULT_KEY = '_count';\n\n/**\n * How many rows each named relation holds per parent, `true` for all of them or a filter to narrow\n * which ones count: a correlated count in the read's own statement, so no related row is loaded. Comes\n * back under `_count`, which keeps it clear of a relation of the same name `$populate` filled.\n */\nexport type QueryCount<E, Raw = QueryRaw, R extends keyof E = ToManyRelationKey<E>> = {\n [K in R]?: BooleanLike | QueryFilter<RelationTarget<E[K]>, Raw>;\n};\n\n/**\n * query conflict paths - subset of field keys used to detect upsert conflicts.\n */\nexport type QueryConflictPaths<E> = QuerySelect<E, FieldKey<E>, true>;\n\n/**\n * Options to populate a relation declared as `V`, by its cardinality.\n */\nexport type QueryPopulateRelationOptions<V, Raw = QueryRaw> =\n IsMany<V> extends true\n ? RelationQuery<RelationTarget<V>, Raw>\n : QueryUnique<RelationTarget<V>, Raw> & { $required?: boolean };\n\n/**\n * The per-request context parameterized filters read, set with `withContext(ctx, cb)`. An interface,\n * so its keys can be typed once: `declare module 'uql-orm' { interface UqlContext { tenantId: number } }`.\n */\nexport interface UqlContext {\n [key: string]: unknown;\n}\n\n/**\n * A filter's `$where` fragment: a plain fragment, or a function of the ambient {@link UqlContext}.\n * Return `undefined` when the condition can't resolve (see {@link FilterOptions.onMissing}).\n */\nexport type FilterWhere<E> = QueryWhere<E> | ((context: UqlContext | undefined) => QueryWhere<E> | undefined);\n\n/**\n * What to do when a filter's condition returns `undefined`. `skip` omits it (convenience filters);\n * `throw` fails closed (the default for `security` filters).\n */\nexport type FilterOnMissing = 'skip' | 'throw';\n\n/**\n * Authoring shape for `@Entity({ filters })` / `@Filter` / `defineFilter`.\n */\nexport type FilterOptions<E = unknown> = {\n readonly where: FilterWhere<E>;\n /** Applied to every query unless bypassed via `QueryOptions.filters`. Defaults to `true`. */\n readonly default?: boolean;\n} & (\n | {\n readonly security?: false;\n /** What to do when {@link FilterOptions.where} returns `undefined`. Defaults to `skip`. */\n readonly onMissing?: FilterOnMissing;\n }\n | {\n /**\n * Row-level-security filter: always applied (ignores `QueryOptions.filters` bypass) and\n * AND-merged so a client `$where` on the same field can't override it. It fails closed.\n */\n readonly security: true;\n readonly onMissing?: 'throw';\n }\n);\n\n/**\n * direction for the sort.\n */\nexport type QuerySortDirection = -1 | 1 | 'asc' | 'desc';\n\n/**\n * Accepted value for a field in `$sort` - either a direction or a vector similarity search.\n */\nexport type QuerySortValue = QuerySortDirection | QueryVectorSearch;\n\n/**\n * Ordering parents by how many rows a to-many relation holds - \"the ten users with the most posts\".\n * The tally is computed per parent as a correlated count, never by loading the rows.\n */\nexport type QuerySortByCount = {\n $count: QuerySortDirection;\n};\n\n/**\n * A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count`, or a vector distance,\n * which `Vector` confines to the queried entity. One mapped type over the key sets: an intersection is\n * checked once per member, which made this the costliest type to check.\n */\nexport type QuerySortMap<E, Vector extends boolean = true, K extends keyof E = FieldKey<E> | RelationKey<E>> = {\n [P in K]?: P extends RelationKey<E>\n ? // A to-many has no single value to order by, so what it offers instead is its own size.\n IsMany<E[P]> extends true\n ? QuerySortByCount\n : QuerySortMap<RelationTarget<E[P]>, false>\n : Vector extends true\n ? NonNullable<E[P]> extends readonly number[]\n ? QuerySortValue\n : QuerySortDirection\n : QuerySortDirection;\n} & ([JsonFieldPaths<E>] extends [never] ? unknown : { [P in JsonFieldPaths<E>]?: QuerySortDirection });\n\n/**\n * pager options.\n */\nexport type QueryPager = {\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * Which rows a statement addresses.\n */\nexport type QueryFilter<E, Raw = QueryRaw> = {\n /**\n * filtering options.\n */\n $where?: QueryWhere<E, Raw>;\n};\n\n/**\n * A filter plus the page `count` takes. No `$sort`: ordering picks *which* rows a page holds, never\n * how many, so a count that accepted one would promise an influence it cannot have.\n */\nexport type QueryPage<E, Raw = QueryRaw> = QueryFilter<E, Raw> & QueryPager;\n\n/**\n * A filter plus the ordering and page `updateMany`/`deleteMany` take. Both settle the\n * rows they address with a SELECT first, so the page is portable rather than MySQL-only, and a\n * vector `$sort` is as valid here as on a read: it ranks the settle query's rows, which has the\n * projection list to hold the distance. `$lock` stays off these, declared on {@link Query} instead.\n */\nexport type QuerySearch<E, Raw = QueryRaw> = QueryPage<E, Raw> & {\n /**\n * sorting options.\n */\n $sort?: QuerySortMap<E>;\n};\n\n/**\n * query options.\n */\nexport type Query<E, Raw = QueryRaw> = {\n /**\n * field selection - `{ name: true }` whitelists fields, or raw SQL projections\n * (``[raw`LOG10(points)`.as('score')]``, SQL dialects only - MongoDB rejects the raw-array form).\n * Mutually exclusive with `$exclude`.\n */\n $select?: QuerySelectValue<E, Raw>;\n\n /**\n * relation population options.\n */\n $populate?: QueryPopulate<E, Raw>;\n\n /**\n * how many rows each named relation holds, under `_count` on every row. See {@link QueryCount}.\n */\n $count?: QueryCount<E, Raw>;\n\n /**\n * field exclusion - `{ name: true }` blacklists fields. Mutually exclusive with positive `$select`.\n * Keys a relation is assembled from (a joined row's primary key, a to-many's foreign key) are kept\n * regardless, since subtracting them would leave the relation unfilled.\n */\n $exclude?: QueryExclude<E>;\n\n /**\n * sorting options, vector similarity search included: a SELECT is the one statement with a\n * projection list to hold the distance such a search computes.\n */\n $sort?: QuerySortMap<E>;\n\n /**\n * whether to return only distinct rows.\n */\n $distinct?: boolean;\n\n /**\n * Lock the rows this query returns, `SELECT ... FOR UPDATE`, inside an open transaction: outside one\n * it is refused, since the lock would drop before the rows are used. SQL only, and not the SQLite family.\n */\n $lock?: QueryLock;\n\n /**\n * How many candidates an ANN index explores before ranking a vector search, in that index's own units\n * (`hnsw.ef_search`, `numCandidates`...); ignored where the search is exact. Postgres needs a transaction.\n */\n $candidates?: number;\n\n // `$where`, `$skip` and `$limit` are declared here rather than intersected in from\n // {@link QueryFilter} and {@link QueryPager}: an assignability check against an intersection is\n // repeated per constituent, and every query in a consuming codebase pays that. The two shapes are\n // pinned together in `queryStatementClauses.test-d.ts` so the copies cannot drift.\n\n /**\n * filtering options.\n */\n $where?: QueryWhere<E, Raw>;\n\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * A {@link Query} as it travels as JSON, which a `raw` SQL fragment cannot: what the browser client takes,\n * and what an RPC contract (tRPC, oRPC, TanStack Start) declares as its input.\n */\nexport type WireQuery<E> = Query<E, never>;\n\n/**\n * `Query`'s clauses grouped by the shape of their value, for the wire parser and the relation query\n * check alike; `satisfies` keeps them in step with `Query`.\n */\nexport const QUERY_OBJECT_CLAUSES = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Object clauses only the statement itself takes: a populated relation's rows keep their declared type,\n * so a `$count` inside one would have no `_count` to land in.\n */\nexport const QUERY_ROOT_OBJECT_CLAUSES = ['$count'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_NUMBER_CLAUSES = ['$skip', '$limit'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Number clauses only the statement itself takes - the numeric mirror of {@link QUERY_ROOT_OBJECT_CLAUSES}.\n * `$candidates` tunes the index behind a vector search, and a vector search only ever ranks the rows\n * the statement returns, so a relation's own query has nothing to tune.\n */\nexport const QUERY_ROOT_NUMBER_CLAUSES = ['$candidates'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_BOOLEAN_CLAUSES = ['$distinct'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/** The clauses that describe the statement, which a populated relation's own query refuses by name. */\nexport const QUERY_STATEMENT_CLAUSES = [\n '$lock',\n ...QUERY_ROOT_OBJECT_CLAUSES,\n ...QUERY_ROOT_NUMBER_CLAUSES,\n] as const satisfies readonly (keyof Query<unknown>)[];\n\ntype RelationClause = (\n | typeof QUERY_OBJECT_CLAUSES\n | typeof QUERY_NUMBER_CLAUSES\n | typeof QUERY_BOOLEAN_CLAUSES\n)[number];\n\n/**\n * A populated relation's own query: the clause groups its runtime check accepts, so the two cannot\n * drift, and a clause added to {@link Query} stays off it until it joins one of them.\n */\nexport type RelationQuery<E = object, Raw = QueryRaw> = Pick<Query<E, Raw>, RelationClause> & {\n $required?: boolean;\n};\n\n/**\n * options to get a single record.\n */\nexport type QueryOne<E, Raw = QueryRaw> = Except<Query<E, Raw>, '$limit'>;\n\n/**\n * options to get an unique record.\n */\nexport type QueryUnique<E, Raw = QueryRaw> = Pick<QueryOne<E, Raw>, '$select' | '$exclude' | '$populate' | '$where'>;\n\n/**\n * The clauses that shape a row, captured as key sets rather than maps: a naked type parameter skips\n * excess-property checks, while a key set fails its own constraint on a typo.\n * @internal\n */\ntype QueryProjection<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n Raw = QueryRaw,\n> = {\n $select?: QuerySelect<E, S, V> | readonly Raw[];\n $exclude?: QuerySelect<E, X, V>;\n $populate?: QueryPopulate<E, Raw, P>;\n // Narrowing the captured names to the to-many ones leaves a to-one relation no key here at all,\n // so counting one is an excess property rather than a value to check.\n $count?: QueryCount<E, Raw, C & ToManyRelationKey<E>>;\n};\n\n/**\n * A {@link Query} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n Raw = QueryRaw,\n> = Query<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;\n\n/**\n * A {@link QueryOne} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryOneProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n Raw = QueryRaw,\n> = QueryOne<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;\n\n/**\n * The keys a query comes back with, as the runtime projects them: a positive `$select`'s, or every\n * field minus what `$select` or `$exclude` subtracts, plus the populated relations.\n * @internal\n */\ntype ProjectedKeys<E, S, V, X, P> =\n | ([V] extends [false | 0] ? Exclude<FieldKey<E>, S> : [S] extends [never] ? Exclude<FieldKey<E>, X> : S)\n | P;\n\n/**\n * Whether every entry of the captured map says the same thing: all selected, or all subtracted.\n * @internal\n */\ntype IsUniform<V> = [V] extends [true | 1] ? true : [V] extends [false | 0] ? true : false;\n\n/**\n * A find's row: the entity narrowed to what the query projected and populated, so reading anything\n * else does not compile. The entity itself where the projection is raw, absent or not uniform.\n * @example `QueryFindResult<User, 'id' | 'name'>`\n */\nexport type QueryFindResult<\n E,\n S extends FieldKey<E> = never,\n // A whitelist by default, so the hand-written form reads `QueryFindResult<User, 'id' | 'name'>`.\n V = true,\n X extends FieldKey<E> = never,\n P extends RelationKey<E> = never,\n C extends RelationKey<E> = never,\n> = QueryProjectedRow<E, S, V, X, P, C> & CountedRelations<C>;\n\n/**\n * The `_count` a query asked for, or an inert intersection member when it asked for none - so a read\n * without `$count` keeps exactly the row type it had.\n */\ntype CountedRelations<C extends PropertyKey> = [C] extends [never]\n ? unknown\n : { [K in typeof COUNT_RESULT_KEY]: { [R in C]: number } };\n\n/** @internal */\ntype QueryProjectedRow<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n> = [S | X] extends [never]\n ? E\n : IsUniform<V> extends true\n ? [PopulatedToMany<E, P>] extends [never]\n ? // `Pick`, not a key remap: an entity keyed by an index signature - a content type defined at\n // runtime - has `string` for its keys, and a remap keeps no literal one, so every projection\n // over one came back as `{}`.\n Pick<E, ProjectedKeys<E, S, V, X, P> & keyof E>\n : // A populated to-many is always a list, empty where the parent has no children, so it maps\n // and counts without a guard. Only that promotion needs a second member, and only a query\n // that populates one pays for it; every other key keeps the modifier the entity declared,\n // a to-one relation included, since a join that finds no row leaves it absent.\n Pick<E, Exclude<ProjectedKeys<E, S, V, X, P>, PopulatedToMany<E, P>> & keyof E> & {\n [K in PopulatedToMany<E, P>]-?: NonNullable<E[K]>;\n }\n : E;\n\n/** The to-many relations a query populated, which come back as lists rather than as optional ones. */\ntype PopulatedToMany<E, P> = Extract<P, ToManyRelationKey<E>>;\n\n/**\n * stringified query.\n */\nexport type QueryStringified = {\n [K in keyof Query<unknown>]?: string;\n};\n\n/** What upserting one row reports. `created` is only knowable for a single statement, so a batch has none. */\nexport type QueryUpsertOneResult<E> = {\n readonly id?: WrittenId<E>;\n readonly changes?: number;\n /** Whether the record was created (`true`) or updated (`false`), where the dialect can tell. */\n readonly created?: boolean;\n};\n\n/**\n * What upserting many rows reports. `ids` is payload-aligned like an insert's, so it zips with the\n * rows that were passed, and carries a composite key as the map naming it.\n */\nexport type QueryUpsertManyResult<E> = {\n readonly ids: (WrittenId<E> | undefined)[];\n readonly changes?: number;\n};\n\n/**\n * result of an update operation, as the driver reports it - which is what `run` hands back, where\n * there is no entity to name the ids against. The `QueryUpsert*Result` pair is the entity-level shape.\n */\nexport type QueryUpdateResult = {\n /**\n * number of affected records.\n */\n changes?: number;\n /**\n * the IDs the statement reported, in payload order, `undefined` where it reported none for that\n * row - a MongoDB upsert names only the documents it inserted. Exact on `'returning'` dialects;\n * inferred from the driver header on the others (see {@link InsertIdSource}), and absent\n * altogether when the header reports nothing.\n */\n ids?: (PrimaryKey | undefined)[];\n /**\n * first inserted ID.\n */\n firstId?: PrimaryKey;\n /**\n * whether the record was created (`true`) or updated (`false`).\n * `undefined` when the dialect cannot determine this (e.g. SQLite).\n */\n created?: boolean;\n};\n",
6
+ "import type { FieldKey, JsonFieldPaths, RelationKey, RelationTarget, ToManyRelationKey, WrittenId } from './entity.js';\nimport type { QueryLock } from './queryLock.js';\nimport type { QueryRaw } from './queryRaw.js';\nimport type { QueryWhere } from './queryWhere.js';\nimport type { BooleanLike, Except, IsMany, PrimaryKey } from './utility.js';\nimport type { QueryVectorSearch } from './vector.js';\n\nexport type QueryOptions = {\n /**\n * Toggle named entity filters for this query. `false` disables all filters;\n * `{ softDelete: false }` disables one; `{ myFilter: true }` force-enables a `default: false` filter.\n * Security filters cannot be disabled here.\n */\n filters?: false | Record<string, boolean>;\n /**\n * Delete only: physically remove rows instead of soft-deleting, ignoring the soft-delete filter so\n * already-deleted rows are removed too. No effect on entities without a soft-delete field.\n */\n hardDelete?: boolean;\n /**\n * `updateMany`/`deleteMany` only: address every row of the table on purpose. Without it a bulk write\n * that names none - no `$where` and no `$limit` - is refused, since a forgotten filter and the whole\n * table look alike. The entity's own filters never count as naming one.\n */\n unfiltered?: boolean;\n /**\n * prefix the query with this.\n */\n prefix?: string;\n /**\n * automatically infer the prefix for the query.\n */\n autoPrefix?: boolean;\n};\n\n/**\n * Field selection - `{ name: true }` whitelists fields; relations go in `$populate`. Declared over\n * `F extends keyof E`, like every map keyed by an entity's members, so each key stays linked to its\n * property and an editor rename reaches it. `F` is also how a projection passes its captured key set.\n */\nexport type QuerySelect<E, F extends keyof E = FieldKey<E>, V = BooleanLike> = {\n [K in F]?: V;\n};\n\n/**\n * Accepted `$select` value: a field map, or raw SQL projections built with `raw()`\n * (e.g. ``[raw`*`, raw`LOG10(points)`.as('score')]``). The raw form is SQL-only.\n */\nexport type QuerySelectValue<E, Raw = QueryRaw> = QuerySelect<E> | readonly Raw[];\n\n/**\n * Fields to exclude from the query result - `{ name: true }` blacklists fields.\n * Mutually exclusive with positive field selections in `$select`.\n */\nexport type QueryExclude<E> = QuerySelect<E>;\n\n/**\n * relation population map.\n */\nexport type QueryPopulate<E, Raw = QueryRaw, R extends keyof E = RelationKey<E>> = {\n [K in R]?: BooleanLike | QueryPopulateRelationOptions<E[K], Raw>;\n};\n\n/**\n * The key a read carries its relation tallies under. One spelling for the type and the runtime that\n * fills it: they sit in different modules, so a drift would type-check and answer `undefined`.\n */\nexport const COUNT_RESULT_KEY = '_count';\n\n/**\n * How many rows each named relation holds per parent, `true` for all of them or a filter to narrow\n * which ones count: a correlated count in the read's own statement, so no related row is loaded. Comes\n * back under `_count`, which keeps it clear of a relation of the same name `$populate` filled.\n */\nexport type QueryCount<E, Raw = QueryRaw, R extends keyof E = ToManyRelationKey<E>> = {\n [K in R]?: BooleanLike | QueryFilter<RelationTarget<E[K]>, Raw>;\n};\n\n/**\n * query conflict paths - subset of field keys used to detect upsert conflicts.\n */\nexport type QueryConflictPaths<E> = QuerySelect<E, FieldKey<E>, true>;\n\n/**\n * Options to populate a relation declared as `V`, by its cardinality.\n */\nexport type QueryPopulateRelationOptions<V, Raw = QueryRaw> =\n IsMany<V> extends true\n ? RelationQuery<RelationTarget<V>, Raw>\n : QueryUnique<RelationTarget<V>, Raw> & { $required?: boolean };\n\n/**\n * The per-request context parameterized filters read, set with `withContext(ctx, cb)`. An interface,\n * so its keys can be typed once: `declare module 'uql-orm' { interface UqlContext { tenantId: number } }`.\n */\nexport interface UqlContext {\n [key: string]: unknown;\n}\n\n/**\n * A filter's `$where` fragment: a plain fragment, or a function of the ambient {@link UqlContext}.\n * Return `undefined` when the condition can't resolve (see {@link FilterOptions.onMissing}).\n */\nexport type FilterWhere<E> = QueryWhere<E> | ((context: UqlContext | undefined) => QueryWhere<E> | undefined);\n\n/**\n * What to do when a filter's condition returns `undefined`. `skip` omits it (convenience filters);\n * `throw` fails closed (the default for `security` filters).\n */\nexport type FilterOnMissing = 'skip' | 'throw';\n\n/**\n * Authoring shape for `@Entity({ filters })` / `@Filter` / `defineFilter`.\n */\nexport type FilterOptions<E = unknown> = {\n readonly where: FilterWhere<E>;\n /** Applied to every query unless bypassed via `QueryOptions.filters`. Defaults to `true`. */\n readonly default?: boolean;\n} & (\n | {\n readonly security?: false;\n /** What to do when {@link FilterOptions.where} returns `undefined`. Defaults to `skip`. */\n readonly onMissing?: FilterOnMissing;\n }\n | {\n /**\n * Row-level-security filter: always applied (ignores `QueryOptions.filters` bypass) and\n * AND-merged so a client `$where` on the same field can't override it. It fails closed.\n */\n readonly security: true;\n readonly onMissing?: 'throw';\n }\n);\n\n/**\n * direction for the sort.\n */\nexport type QuerySortDirection = -1 | 1 | 'asc' | 'desc';\n\n/**\n * Accepted value for a field in `$sort` - either a direction or a vector similarity search.\n */\nexport type QuerySortValue = QuerySortDirection | QueryVectorSearch;\n\n/**\n * Ordering parents by how many rows a to-many relation holds - \"the ten users with the most posts\".\n * The tally is computed per parent as a correlated count, never by loading the rows.\n */\nexport type QuerySortByCount = {\n $count: QuerySortDirection;\n};\n\n/**\n * Ordering by relevance to the `$text` at the root of `$where`: most relevant first, the one order every\n * engine ranks by (MongoDB's `textScore` sorts no other way).\n */\nexport type QuerySortByText = {\n $text?: -1 | 'desc';\n};\n\n/**\n * A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count`, or a vector distance or\n * `$text` relevance, which `Vector` confines to the queried entity. One mapped type over the key sets: an\n * intersection is checked once per member, which made this the costliest type to check.\n */\nexport type QuerySortMap<E, Vector extends boolean = true, K extends keyof E = FieldKey<E> | RelationKey<E>> = {\n [P in K]?: P extends RelationKey<E>\n ? // A to-many has no single value to order by, so what it offers instead is its own size.\n IsMany<E[P]> extends true\n ? QuerySortByCount\n : QuerySortMap<RelationTarget<E[P]>, false>\n : Vector extends true\n ? NonNullable<E[P]> extends readonly number[]\n ? QuerySortValue\n : QuerySortDirection\n : QuerySortDirection;\n} & ([JsonFieldPaths<E>] extends [never] ? unknown : { [P in JsonFieldPaths<E>]?: QuerySortDirection }) &\n (Vector extends true ? QuerySortByText : unknown);\n\n/**\n * pager options.\n */\nexport type QueryPager = {\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * Which rows a statement addresses.\n */\nexport type QueryFilter<E, Raw = QueryRaw> = {\n /**\n * filtering options.\n */\n $where?: QueryWhere<E, Raw>;\n};\n\n/**\n * A filter plus the page `count` takes. No `$sort`: ordering picks *which* rows a page holds, never\n * how many, so a count that accepted one would promise an influence it cannot have.\n */\nexport type QueryPage<E, Raw = QueryRaw> = QueryFilter<E, Raw> & QueryPager;\n\n/**\n * A filter plus the ordering and page `updateMany`/`deleteMany` take. Both settle the\n * rows they address with a SELECT first, so the page is portable rather than MySQL-only, and a\n * vector `$sort` is as valid here as on a read: it ranks the settle query's rows, which has the\n * projection list to hold the distance. `$lock` stays off these, declared on {@link Query} instead.\n */\nexport type QuerySearch<E, Raw = QueryRaw> = QueryPage<E, Raw> & {\n /**\n * sorting options.\n */\n $sort?: QuerySortMap<E>;\n};\n\n/**\n * query options.\n */\nexport type Query<E, Raw = QueryRaw> = {\n /**\n * field selection - `{ name: true }` whitelists fields, or raw SQL projections\n * (``[raw`LOG10(points)`.as('score')]``, SQL dialects only - MongoDB rejects the raw-array form).\n * Mutually exclusive with `$exclude`.\n */\n $select?: QuerySelectValue<E, Raw>;\n\n /**\n * relation population options.\n */\n $populate?: QueryPopulate<E, Raw>;\n\n /**\n * how many rows each named relation holds, under `_count` on every row. See {@link QueryCount}.\n */\n $count?: QueryCount<E, Raw>;\n\n /**\n * field exclusion - `{ name: true }` blacklists fields. Mutually exclusive with positive `$select`.\n * Keys a relation is assembled from (a joined row's primary key, a to-many's foreign key) are kept\n * regardless, since subtracting them would leave the relation unfilled.\n */\n $exclude?: QueryExclude<E>;\n\n /**\n * sorting options, vector similarity search included: a SELECT is the one statement with a\n * projection list to hold the distance such a search computes.\n */\n $sort?: QuerySortMap<E>;\n\n /**\n * whether to return only distinct rows.\n */\n $distinct?: boolean;\n\n /**\n * Lock the rows this query returns, `SELECT ... FOR UPDATE`, inside an open transaction: outside one\n * it is refused, since the lock would drop before the rows are used. SQL only, and not the SQLite family.\n */\n $lock?: QueryLock;\n\n /**\n * How many candidates an ANN index explores before ranking a vector search, in that index's own units\n * (`hnsw.ef_search`, `numCandidates`...); ignored where the search is exact. Postgres needs a transaction.\n */\n $candidates?: number;\n\n // `$where`, `$skip` and `$limit` are declared here rather than intersected in from\n // {@link QueryFilter} and {@link QueryPager}: an assignability check against an intersection is\n // repeated per constituent, and every query in a consuming codebase pays that. The two shapes are\n // pinned together in `queryStatementClauses.test-d.ts` so the copies cannot drift.\n\n /**\n * filtering options.\n */\n $where?: QueryWhere<E, Raw>;\n\n /**\n * Index from where start the search\n */\n $skip?: number;\n\n /**\n * Max number of records to retrieve\n */\n $limit?: number;\n};\n\n/**\n * A {@link Query} as it travels as JSON, which a `raw` SQL fragment cannot: what the browser client takes,\n * and what an RPC contract (tRPC, oRPC, TanStack Start) declares as its input.\n */\nexport type WireQuery<E> = Query<E, never>;\n\n/**\n * `Query`'s clauses grouped by the shape of their value, for the wire parser and the relation query\n * check alike; `satisfies` keeps them in step with `Query`.\n */\nexport const QUERY_OBJECT_CLAUSES = [\n '$select',\n '$populate',\n '$exclude',\n '$where',\n '$sort',\n] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Object clauses only the statement itself takes: a populated relation's rows keep their declared type,\n * so a `$count` inside one would have no `_count` to land in.\n */\nexport const QUERY_ROOT_OBJECT_CLAUSES = ['$count'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_NUMBER_CLAUSES = ['$skip', '$limit'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/**\n * Number clauses only the statement itself takes - the numeric mirror of {@link QUERY_ROOT_OBJECT_CLAUSES}.\n * `$candidates` tunes the index behind a vector search, and a vector search only ever ranks the rows\n * the statement returns, so a relation's own query has nothing to tune.\n */\nexport const QUERY_ROOT_NUMBER_CLAUSES = ['$candidates'] as const satisfies readonly (keyof Query<unknown>)[];\n\nexport const QUERY_BOOLEAN_CLAUSES = ['$distinct'] as const satisfies readonly (keyof Query<unknown>)[];\n\n/** The clauses that describe the statement, which a populated relation's own query refuses by name. */\nexport const QUERY_STATEMENT_CLAUSES = [\n '$lock',\n ...QUERY_ROOT_OBJECT_CLAUSES,\n ...QUERY_ROOT_NUMBER_CLAUSES,\n] as const satisfies readonly (keyof Query<unknown>)[];\n\ntype RelationClause = (\n | typeof QUERY_OBJECT_CLAUSES\n | typeof QUERY_NUMBER_CLAUSES\n | typeof QUERY_BOOLEAN_CLAUSES\n)[number];\n\n/**\n * A populated relation's own query: the clause groups its runtime check accepts, so the two cannot\n * drift, and a clause added to {@link Query} stays off it until it joins one of them.\n */\nexport type RelationQuery<E = object, Raw = QueryRaw> = Pick<Query<E, Raw>, RelationClause> & {\n $required?: boolean;\n};\n\n/**\n * options to get a single record.\n */\nexport type QueryOne<E, Raw = QueryRaw> = Except<Query<E, Raw>, '$limit'>;\n\n/**\n * options to get an unique record.\n */\nexport type QueryUnique<E, Raw = QueryRaw> = Pick<QueryOne<E, Raw>, '$select' | '$exclude' | '$populate' | '$where'>;\n\n/**\n * The clauses that shape a row, captured as key sets rather than maps: a naked type parameter skips\n * excess-property checks, while a key set fails its own constraint on a typo.\n * @internal\n */\ntype QueryProjection<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n Raw = QueryRaw,\n> = {\n $select?: QuerySelect<E, S, V> | readonly Raw[];\n $exclude?: QuerySelect<E, X, V>;\n $populate?: QueryPopulate<E, Raw, P>;\n // Narrowing the captured names to the to-many ones leaves a to-one relation no key here at all,\n // so counting one is an excess property rather than a value to check.\n $count?: QueryCount<E, Raw, C & ToManyRelationKey<E>>;\n};\n\n/**\n * A {@link Query} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n Raw = QueryRaw,\n> = Query<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;\n\n/**\n * A {@link QueryOne} whose projection is captured, so {@link QueryFindResult} can shape the row.\n */\nexport type QueryOneProjected<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E> = never,\n Raw = QueryRaw,\n> = QueryOne<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;\n\n/**\n * The keys a query comes back with, as the runtime projects them: a positive `$select`'s, or every\n * field minus what `$select` or `$exclude` subtracts, plus the populated relations.\n * @internal\n */\ntype ProjectedKeys<E, S, V, X, P> =\n | ([V] extends [false | 0] ? Exclude<FieldKey<E>, S> : [S] extends [never] ? Exclude<FieldKey<E>, X> : S)\n | P;\n\n/**\n * Whether every entry of the captured map says the same thing: all selected, or all subtracted.\n * @internal\n */\ntype IsUniform<V> = [V] extends [true | 1] ? true : [V] extends [false | 0] ? true : false;\n\n/**\n * A find's row: the entity narrowed to what the query projected and populated, so reading anything\n * else does not compile. The entity itself where the projection is raw, absent or not uniform.\n * @example `QueryFindResult<User, 'id' | 'name'>`\n */\nexport type QueryFindResult<\n E,\n S extends FieldKey<E> = never,\n // A whitelist by default, so the hand-written form reads `QueryFindResult<User, 'id' | 'name'>`.\n V = true,\n X extends FieldKey<E> = never,\n P extends RelationKey<E> = never,\n C extends RelationKey<E> = never,\n> = QueryProjectedRow<E, S, V, X, P, C> & CountedRelations<C>;\n\n/**\n * The `_count` a query asked for, or an inert intersection member when it asked for none - so a read\n * without `$count` keeps exactly the row type it had.\n */\ntype CountedRelations<C extends PropertyKey> = [C] extends [never]\n ? unknown\n : { [K in typeof COUNT_RESULT_KEY]: { [R in C]: number } };\n\n/** @internal */\ntype QueryProjectedRow<\n E,\n S extends FieldKey<E>,\n V,\n X extends FieldKey<E>,\n P extends RelationKey<E>,\n C extends RelationKey<E>,\n> = [S | X] extends [never]\n ? E\n : IsUniform<V> extends true\n ? [PopulatedToMany<E, P>] extends [never]\n ? // `Pick`, not a key remap: an entity keyed by an index signature - a content type defined at\n // runtime - has `string` for its keys, and a remap keeps no literal one, so every projection\n // over one came back as `{}`.\n Pick<E, ProjectedKeys<E, S, V, X, P> & keyof E>\n : // A populated to-many is always a list, empty where the parent has no children, so it maps\n // and counts without a guard. Only that promotion needs a second member, and only a query\n // that populates one pays for it; every other key keeps the modifier the entity declared,\n // a to-one relation included, since a join that finds no row leaves it absent.\n Pick<E, Exclude<ProjectedKeys<E, S, V, X, P>, PopulatedToMany<E, P>> & keyof E> & {\n [K in PopulatedToMany<E, P>]-?: NonNullable<E[K]>;\n }\n : E;\n\n/** The to-many relations a query populated, which come back as lists rather than as optional ones. */\ntype PopulatedToMany<E, P> = Extract<P, ToManyRelationKey<E>>;\n\n/**\n * stringified query.\n */\nexport type QueryStringified = {\n [K in keyof Query<unknown>]?: string;\n};\n\n/** What upserting one row reports. `created` is only knowable for a single statement, so a batch has none. */\nexport type QueryUpsertOneResult<E> = {\n readonly id?: WrittenId<E>;\n readonly changes?: number;\n /** Whether the record was created (`true`) or updated (`false`), where the dialect can tell. */\n readonly created?: boolean;\n};\n\n/**\n * What upserting many rows reports. `ids` is payload-aligned like an insert's, so it zips with the\n * rows that were passed, and carries a composite key as the map naming it.\n */\nexport type QueryUpsertManyResult<E> = {\n readonly ids: (WrittenId<E> | undefined)[];\n readonly changes?: number;\n};\n\n/**\n * result of an update operation, as the driver reports it - which is what `run` hands back, where\n * there is no entity to name the ids against. The `QueryUpsert*Result` pair is the entity-level shape.\n */\nexport type QueryUpdateResult = {\n /**\n * number of affected records.\n */\n changes?: number;\n /**\n * the IDs the statement reported, in payload order, `undefined` where it reported none for that\n * row - a MongoDB upsert names only the documents it inserted. Exact on `'returning'` dialects;\n * inferred from the driver header on the others (see {@link InsertIdSource}), and absent\n * altogether when the header reports nothing.\n */\n ids?: (PrimaryKey | undefined)[];\n /**\n * first inserted ID.\n */\n firstId?: PrimaryKey;\n /**\n * whether the record was created (`true`) or updated (`false`).\n * `undefined` when the dialect cannot determine this (e.g. SQLite).\n */\n created?: boolean;\n};\n",
7
7
  "import type { QueryContext, RelationAggregateSpec, SqlQueryDialect } from './dialect.js';\nimport type { Type } from './utility.js';\n\n/** What a `raw` callback receives. See {@link QueryRawFn}. */\nexport type QueryRawRenderOptions = {\n /** The dialect rendering the SQL. */\n dialect: SqlQueryDialect;\n /** The alias of the table in scope, unescaped; empty where there is none. */\n prefix: string;\n /** {@link prefix} escaped, with its trailing dot. */\n escapedPrefix: string;\n /** The query context the SQL is written into. */\n ctx: QueryContext;\n /**\n * The entity being rendered, which a ref read off a definition's map resolves its column against: a\n * computed field's own, or the one whose schema is built. Absent where a statement renders SQL.\n */\n entity?: Type<unknown>;\n};\n\n/** {@link QueryRawRenderOptions} as the callers along the way fill them in, every one still optional. */\nexport type QueryRawFnOptions = Partial<QueryRawRenderOptions>;\n\n/**\n * A `raw` callback: write into `ctx`, or return a string or number to have it appended. Anything else\n * it returns is ignored, which is why the return type is `unknown` rather than `void | Scalar` - the\n * latter rejected `({ ctx }) => ctx.append(...)`, the form every computed field is written in, because\n * TypeScript's \"returning a value where void is expected\" allowance does not apply to a union.\n */\nexport type QueryRawFn = (opts: QueryRawRenderOptions) => unknown;\n\nexport const RAW_VALUE: unique symbol = Symbol('rawValue');\nexport const RAW_ALIAS: unique symbol = Symbol('rawAlias');\n\nexport class QueryRaw {\n readonly [RAW_VALUE]: QueryRawFn;\n readonly [RAW_ALIAS]?: string;\n\n constructor(value: QueryRawFn, alias?: string) {\n this[RAW_VALUE] = value;\n this[RAW_ALIAS] = alias;\n }\n\n /** The same expression under an alias, for a `$select` projection. */\n as(alias: string): QueryRaw {\n return new QueryRaw(this[RAW_VALUE], alias);\n }\n\n /** Writes the expression into `opts.ctx`. The alias is the projection's to write, after the term. */\n render(opts: QueryRawRenderOptions): void {\n const emitted = this[RAW_VALUE](opts);\n if (typeof emitted === 'string' || (typeof emitted === 'number' && !Number.isNaN(emitted))) {\n opts.ctx.append(String(emitted));\n }\n }\n}\n\n/**\n * A field of an entity as SQL, read off `refs(Entity)` or a definition's refs: interpolated into `raw`, it\n * renders as the field's column. Its `key` is how an index tells a column from an expression.\n */\nexport class ColumnRef<K extends string = string> extends QueryRaw {\n constructor(\n readonly key: K,\n value: QueryRawFn,\n ) {\n super(value);\n }\n}\n\n/**\n * A relation aggregate as SQL, read off a `computed` field's refs: `(user) => user.resources.count()`.\n * It renders as the correlated subquery a `$count` reads, so a field holding one is read, filtered and\n * sorted like any other.\n *\n * `V` is the value it reads and `Storable` whether a trigger could keep it, both carried in phantom\n * fields so the aggregate a field declares decides the property's type and refuses `stored: true` on\n * one no delta can maintain.\n */\nexport class RelationAggregate<V = unknown, Storable extends boolean = boolean> extends QueryRaw {\n declare private readonly __value: V;\n declare private readonly __storable: Storable;\n\n constructor(\n /** What it reads, kept beside the SQL so a read decodes the value the way the target's field does. */\n readonly spec: RelationAggregateSpec,\n value: QueryRawFn,\n ) {\n super(value);\n }\n}\n",
8
8
  "import type { EntityMeta } from '../type/index.js';\n\nexport function throwPendingTransaction(): never {\n throw TypeError('pending transaction');\n}\n\nexport function throwNoPendingTransaction(): never {\n throw TypeError('not a pending transaction');\n}\n\nexport function clone<T>(value: T): T {\n if (typeof value !== 'object' || value === null) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map((it) => clone(it)) as T;\n }\n return { ...value };\n}\n\n/** Whether `obj` has at least one enumerable key. Narrows away `undefined`/`null` for callers. */\nexport function hasKeys<T>(obj: T): obj is NonNullable<T> {\n if (typeof obj !== 'object' || obj === null) return false;\n for (const _ in obj) return true;\n return false;\n}\n\n/**\n * Whether any enumerable key of `obj` satisfies `pred`, short-circuiting on the first match\n * without materializing a key array (unlike `Object.keys(obj).some(pred)`).\n */\nexport function someKey<T extends object>(obj: T, pred: (key: keyof T & string) => boolean): boolean {\n for (const key in obj) {\n if (pred(key)) return true;\n }\n return false;\n}\n\n/** Whether `key` names an operator (`$eq`, `$push`...) rather than a field. */\nexport function isOperatorKey(key: string): boolean {\n return key.startsWith('$');\n}\n\n/** Whether `value` is a non-empty object with an operator key (`$eq`, `$push`...): the one test every dialect classifies with. */\nexport function isOperatorObject(value: unknown): value is Record<string, unknown> {\n return isRecord(value) && someKey(value, isOperatorKey);\n}\n\n/** Whether `value` is an object that is not an array, whose keys can be read. */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nexport function getKeys<T extends object>(obj: T | null | undefined): (keyof T & string)[] {\n return obj ? (Object.keys(obj) as (keyof T & string)[]) : [];\n}\n\n/** The entries of `record` holding a value: a key declared but left `undefined` is no entry at all. */\nexport function definedEntries<K extends string, V>(record: Partial<Record<K, V>>): [K, V][] {\n return (Object.entries(record) as [K, V | undefined][]).filter((entry): entry is [K, V] => entry[1] !== undefined);\n}\n\n/**\n * The entity's own name, declared or its class's. `meta.name` holds only what the author wrote, so\n * the fallback is what an entity that named no table is called - which is why the sites spelling this\n * out reached for three different fallbacks, `?? ''` among them, and named nothing at all.\n */\nexport function entityName<E>(meta: EntityMeta<E>): string {\n return meta.name ?? meta.entity.name;\n}\n\n/**\n * Whether `value` addresses a row by itself rather than naming columns: every primitive, and the\n * object ids a driver deals in (`ObjectId`, `Date`, bytes). Only a plain object names columns, which\n * is what a `$where` map and a composite key's id object both are; an array is a list of either.\n */\nexport function isScalarId(value: unknown): boolean {\n if (typeof value !== 'object' || value === null) {\n return true;\n }\n if (Array.isArray(value)) {\n return false;\n }\n // `null` as well as `Object.prototype`: an object with no prototype is what a query-string parser\n // hands back (`qs`, express's `req.params`), and reading one as a bare id would name one column\n // with a map of several.\n const proto = Object.getPrototypeOf(value);\n return proto !== Object.prototype && proto !== null;\n}\n\n/** Whether `value` is a plain object naming columns, the one shape a `$where` takes. */\nexport function isWhereMap(value: unknown): value is Record<string, unknown> {\n return !Array.isArray(value) && !isScalarId(value);\n}\n",
9
9
  "import type { QueryOptions, WireQuery } from '../type/index.js';\n// the clause lists themselves, not the barrel: this module is in the browser bundle's graph\nimport {\n QUERY_BOOLEAN_CLAUSES,\n QUERY_NUMBER_CLAUSES,\n QUERY_OBJECT_CLAUSES,\n QUERY_ROOT_NUMBER_CLAUSES,\n QUERY_ROOT_OBJECT_CLAUSES,\n} from '../type/query.js';\n// the brand alone, not the class: importing `QueryRaw` for an `instanceof` kept it, and `ColumnRef`\n// with it, in the browser bundle, which is on a size budget\nimport { RAW_VALUE } from '../type/queryRaw.js';\n// the specific util module, not the barrel, so the browser bundle does not pull in entity metadata\nimport { getKeys, isWhereMap } from '../util/object.util.js';\n\n/**\n * Keys accepted from the wire - query structure ({@link Query}) plus the `hardDelete`/`count` scalar\n * flags. Anything else (e.g. `filters`, `context`, `$entity`) is dropped so a remote client can't\n * bypass a security filter or inject ambient context - those are server-only. The `satisfies` ties\n * every entry to a real query/option key, so a typo or a renamed option fails to compile.\n */\nconst ALLOWED_QUERY_KEYS = new Set<string>([\n ...QUERY_OBJECT_CLAUSES,\n ...QUERY_ROOT_OBJECT_CLAUSES,\n ...QUERY_NUMBER_CLAUSES,\n ...QUERY_ROOT_NUMBER_CLAUSES,\n ...QUERY_BOOLEAN_CLAUSES,\n 'hardDelete',\n 'count',\n] satisfies (keyof WireQuery<unknown> | keyof Pick<QueryOptions, 'hardDelete'> | 'count')[]);\n\n/**\n * Keys that mean something locally but that this transport can never honor, so they are rejected\n * rather than dropped like the rest. Each request runs on its own auto-committing connection, so a\n * row lock taken here is released before the response is written: honoring `$lock` is impossible,\n * and ignoring it would hand the caller a read they believe is serialized and is not.\n */\nconst REJECTED_QUERY_KEYS = new Set<string>(['$lock'] satisfies (keyof WireQuery<unknown>)[]);\n\n/**\n * Parse raw query-string entries (with JSON-stringified values) into a UQL query object.\n * Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.\n */\nexport function parseQueryParams(params: Record<string, unknown> = {}): WireQuery<unknown> {\n const query: Record<string, unknown> = {};\n for (const key of getKeys(params)) {\n if (REJECTED_QUERY_KEYS.has(key)) {\n throw Object.assign(new TypeError(`'${key}' is not supported over HTTP`), { status: 400 });\n }\n if (ALLOWED_QUERY_KEYS.has(key)) {\n query[key] = params[key];\n }\n }\n\n for (const key of [...QUERY_OBJECT_CLAUSES, ...QUERY_ROOT_OBJECT_CLAUSES]) {\n const value = query[key];\n if (typeof value === 'string') {\n try {\n query[key] = JSON.parse(value);\n } catch {\n throw Object.assign(new SyntaxError(`invalid JSON in '${key}'`), { status: 400 });\n }\n }\n }\n\n query['$where'] ??= {};\n if (!isWhereMap(query['$where'])) {\n throw Object.assign(new TypeError(\"'$where' must be a JSON object\"), { status: 400 });\n }\n\n // A query string carries every value as text, so what decodes a clause is the shape its group\n // declares. `'false'` is the reason the boolean pass exists rather than the raw value being taken:\n // it is a non-empty string, so a `$distinct=false` would otherwise read as asking for one.\n for (const key of [...QUERY_NUMBER_CLAUSES, ...QUERY_ROOT_NUMBER_CLAUSES]) {\n if (query[key] !== undefined) {\n query[key] = Number(query[key]);\n }\n }\n for (const key of QUERY_BOOLEAN_CLAUSES) {\n if (query[key] !== undefined) {\n query[key] = query[key] === true || query[key] === 'true';\n }\n }\n\n return query as WireQuery<unknown>;\n}\n\n/**\n * Serialize a UQL query object into a percent-encoded query string where object values\n * are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.\n */\nexport function stringifyQuery(query?: Record<string, unknown>): string {\n if (!query) {\n return '';\n }\n const params = new URLSearchParams();\n for (const key of getKeys(query)) {\n const value = query[key];\n if (value === undefined) {\n continue;\n }\n params.append(key, typeof value === 'object' && value !== null ? wireJson(value) : String(value));\n }\n const qs = params.toString();\n return qs ? `?${qs}` : '';\n}\n\n/**\n * What leaves the browser, as JSON, refusing what JSON keeps nothing of rather than letting the server\n * build a statement around the remains. A `raw` fragment renders SQL against a dialect the client does not\n * have and arrives as `{}`; binary arrives as an object keyed by index. A `Date` is not among them - it\n * serializes to ISO 8601, which is what a date column reads. This is what a cast, or a JavaScript caller,\n * hits where the client's types already refuse a fragment.\n */\nexport function wireJson(value: unknown): string {\n return JSON.stringify(value, (_key: string, held: unknown) => {\n if (typeof held !== 'object' || held === null) {\n return held;\n }\n if (RAW_VALUE in held) {\n throw new TypeError('raw SQL cannot travel over HTTP: what leaves the browser is JSON');\n }\n // A blob is a field value, so no type parameter reaches it: this is the only place it is caught.\n if (held instanceof ArrayBuffer || ArrayBuffer.isView(held)) {\n throw new TypeError('binary cannot travel over HTTP: what leaves the browser is JSON');\n }\n return held;\n });\n}\n",
@@ -13,7 +13,7 @@
13
13
  "import { CRUD_ROUTES, entityPath, type HttpMethod } from '../../http/contract.js';\nimport { stringifyQuery } from '../../http/query.js';\nimport type {\n EntityWrite,\n EntityId,\n FieldKey,\n QueryFilter,\n QueryFindResult,\n QueryOneProjected,\n QueryOptions,\n QueryPage,\n QueryProjected,\n QuerySearch,\n RelationKey,\n RequestCountedSuccessResponse,\n RequestSuccessResponse,\n Type,\n UpdateWrite,\n WireQuery,\n WrittenId,\n} from '../../type/index.js';\nimport { isScalarId } from '../../util/object.util.js';\nimport { get, query as httpQuery, patch, post, put, remove } from '../http/index.js';\nimport type { ClientQuerier, RequestFindOptions, RequestOptions } from '../type/index.js';\n\nexport type HttpQuerierDefaults = {\n /**\n * headers sent with every request from this instance, merged under per-call headers.\n * Create one instance per request (e.g. during SSR) to scope auth headers safely.\n */\n readonly headers?: Record<string, string>;\n /**\n * transport for read queries (findOne, findMany, count). 'QUERY' (RFC 10008) sends the\n * JSON query in the request body, avoiding URL-length limits for large queries; requires\n * infrastructure (proxies, CDNs) that forwards the QUERY method. Defaults to 'GET'.\n */\n readonly readMethod?: Extract<HttpMethod, 'GET' | 'QUERY'>;\n /**\n * The URL segment an entity is addressed by, defaulting to its kebab-cased class name - the same\n * option the server handler takes, so one map serves both. State it where the default cannot: a\n * build that minifies class names renames every route.\n */\n readonly entityPath?: (entity: Type<unknown>) => string;\n};\n\n/** The id as one path segment, refusing a composite key, which has no spelling in `/:id` yet. Callers are `async`. */\nfunction idSegment<E>(entity: Type<E>, id: EntityId<E>): string {\n if (!isScalarId(id)) {\n throw new TypeError(`'${entity.name}' was addressed by an id object, which the HTTP route cannot carry.`);\n }\n return String(id);\n}\n\nexport class HttpQuerier implements ClientQuerier {\n constructor(\n readonly basePath: string,\n readonly defaults: HttpQuerierDefaults = {},\n ) {}\n\n async findOneById<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n id: EntityId<E>,\n q?: QueryOneProjected<E, S, V, X, P, C, never>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C> | undefined>> {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return get<QueryFindResult<E, S, V, X, P, C> | undefined>(\n `${basePath}/${idSegment(entity, id)}${qs}`,\n this.buildOptions(opts),\n );\n }\n\n findOne<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n q: QueryOneProjected<E, S, V, X, P, C, never>,\n opts?: RequestOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C> | undefined>> {\n return this.read<QueryFindResult<E, S, V, X, P, C> | undefined>(\n `${this.getBasePath(entity)}${CRUD_ROUTES.findOne.path}`,\n q,\n opts,\n );\n }\n\n findMany<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n q: QueryProjected<E, S, V, X, P, C, never>,\n opts?: RequestFindOptions,\n ): Promise<RequestSuccessResponse<QueryFindResult<E, S, V, X, P, C>[]>> {\n const data: WireQuery<E> & { count?: boolean } = { ...q };\n if (opts?.count) {\n data.count = true;\n }\n return this.read<QueryFindResult<E, S, V, X, P, C>[]>(this.getBasePath(entity), data, opts);\n }\n\n async findManyAndCount<\n E extends object,\n const S extends FieldKey<E> = never,\n const V = true,\n const X extends FieldKey<E> = never,\n const P extends RelationKey<E> = never,\n const C extends RelationKey<E> = never,\n >(\n entity: Type<E>,\n q: QueryProjected<E, S, V, X, P, C, never>,\n opts?: RequestFindOptions,\n ): Promise<RequestCountedSuccessResponse<QueryFindResult<E, S, V, X, P, C>[]>> {\n const response = await this.findMany(entity, q, { ...opts, count: true });\n if (typeof response.count !== 'number') {\n throw new TypeError('findManyAndCount response has an invalid count');\n }\n return { ...response, count: response.count };\n }\n\n count<E extends object>(entity: Type<E>, q?: QueryPage<E, never>, opts?: RequestOptions) {\n return this.read<number>(`${this.getBasePath(entity)}${CRUD_ROUTES.count.path}`, q, opts);\n }\n\n /** The `count` route capped at one row, so existence needs no endpoint of its own. */\n async exists<E extends object>(entity: Type<E>, q?: QueryFilter<E, never>, opts?: RequestOptions) {\n const res = await this.count(entity, { ...q, $limit: 1 }, opts);\n return { ...res, data: res.data > 0 };\n }\n\n insertOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<WrittenId<E> | undefined>(basePath, payload, this.buildOptions(opts));\n }\n\n insertMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return post<(WrittenId<E> | undefined)[]>(\n `${basePath}${CRUD_ROUTES.insertMany.path}`,\n payload,\n this.buildOptions(opts),\n );\n }\n\n async updateOneById<E extends object>(\n entity: Type<E>,\n id: EntityId<E>,\n payload: UpdateWrite<E, never>,\n opts?: RequestOptions,\n ) {\n const basePath = this.getBasePath(entity);\n return patch<number>(`${basePath}/${idSegment(entity, id)}`, payload, this.buildOptions(opts));\n }\n\n updateMany<E extends object>(\n entity: Type<E>,\n q: QuerySearch<E, never>,\n payload: UpdateWrite<E, never>,\n opts?: RequestOptions,\n ) {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(q);\n return patch<number>(`${basePath}${qs}`, payload, this.buildOptions(opts));\n }\n\n saveOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>, opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<WrittenId<E> | undefined>(basePath, payload, this.buildOptions(opts));\n }\n\n saveMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[], opts?: RequestOptions) {\n const basePath = this.getBasePath(entity);\n return put<(WrittenId<E> | undefined)[]>(\n `${basePath}${CRUD_ROUTES.saveMany.path}`,\n payload,\n this.buildOptions(opts),\n );\n }\n\n async deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts: QueryOptions & RequestOptions = {}) {\n const basePath = this.getBasePath(entity);\n const qs = opts.hardDelete ? stringifyQuery({ hardDelete: opts.hardDelete }) : '';\n return remove<number>(`${basePath}/${idSegment(entity, id)}${qs}`, this.buildOptions(opts));\n }\n\n deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E, never>, opts: QueryOptions & RequestOptions = {}) {\n const basePath = this.getBasePath(entity);\n const qs = stringifyQuery(opts.hardDelete ? { ...q, hardDelete: opts.hardDelete } : q);\n return remove<number>(`${basePath}${qs}`, this.buildOptions(opts));\n }\n\n getBasePath<E>(entity: Type<E>) {\n return `${this.basePath}/${(this.defaults.entityPath ?? entityPath)(entity)}`;\n }\n\n protected read<T>(path: string, q: Record<string, unknown> | undefined, opts?: RequestOptions) {\n if (this.defaults.readMethod === 'QUERY') {\n return httpQuery<T>(path, q ?? {}, this.buildOptions(opts));\n }\n return get<T>(`${path}${stringifyQuery(q)}`, this.buildOptions(opts));\n }\n\n protected buildOptions(opts?: RequestOptions): RequestOptions | undefined {\n if (!this.defaults.headers && !opts?.headers) {\n return opts;\n }\n return { ...opts, headers: { ...this.defaults.headers, ...opts?.headers } };\n }\n}\n",
14
14
  "import { HttpQuerier } from './querier/httpQuerier.js';\nimport type { ClientQuerier, ClientQuerierPool } from './type/index.js';\n\nlet defaultPool: ClientQuerierPool = {\n getQuerier: () => new HttpQuerier('/api'),\n};\n\nexport function setQuerierPool<T extends ClientQuerierPool>(pool: T) {\n defaultPool = pool;\n}\n\nexport function getQuerierPool(): ClientQuerierPool {\n return defaultPool;\n}\n\nexport function getQuerier(): ClientQuerier {\n return getQuerierPool().getQuerier();\n}\n"
15
15
  ],
16
- "mappings": "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",
17
- "debugId": "B4B64AF2FFDB750964756E2164756E21",
16
+ "mappings": "AAEA,IAAM,EAAkC,CAAC,EAElC,SAAS,CAAM,CAAC,EAAyC,CAC9D,QAAW,KAAe,EACxB,EAAY,CAAY,EAIrB,SAAS,CAAE,CAAC,EAAiC,CAClD,EAAa,KAAK,CAAE,EACpB,IAAM,EAAQ,EAAa,OAAS,EACpC,MAAO,IAAY,CACjB,EAAa,OAAO,EAAO,CAAC,GCmSzB,IAAM,EAAuB,CAClC,UACA,YACA,WACA,SACA,OACF,EAMa,EAA4B,CAAC,QAAQ,EAErC,EAAuB,CAAC,QAAS,QAAQ,EAOzC,EAA4B,CAAC,aAAa,EAE1C,EAAwB,CAAC,WAAW,EAGpC,EAA0B,CACrC,QACA,GAAG,EACH,GAAG,CACL,EChTO,IAAM,EAA2B,OAAO,UAAU,EAC5C,EAA2B,OAAO,UAAU,ECqBlD,SAAS,CAAyB,CAAC,EAAiD,CACzF,OAAO,EAAO,OAAO,KAAK,CAAG,EAA6B,CAAC,EAsBtD,SAAS,CAAU,CAAC,EAAyB,CAClD,GAAI,OAAO,IAAU,UAAY,IAAU,KACzC,MAAO,GAET,GAAI,MAAM,QAAQ,CAAK,EACrB,MAAO,GAKT,IAAM,EAAQ,OAAO,eAAe,CAAK,EACzC,OAAO,IAAU,OAAO,WAAa,IAAU,KClEjD,IAAM,EAAqB,IAAI,IAAY,CACzC,GAAG,EACH,GAAG,EACH,GAAG,EACH,GAAG,EACH,GAAG,EACH,aACA,OACF,CAA2F,EA8DpF,SAAS,CAAc,CAAC,EAAyC,CACtE,GAAI,CAAC,EACH,MAAO,GAET,IAAM,EAAS,IAAI,gBACnB,QAAW,KAAO,EAAQ,CAAK,EAAG,CAChC,IAAM,EAAQ,EAAM,GACpB,GAAI,IAAU,OACZ,SAEF,EAAO,OAAO,EAAK,OAAO,IAAU,UAAY,IAAU,KAAO,EAAS,CAAK,EAAI,OAAO,CAAK,CAAC,EAElG,IAAM,EAAK,EAAO,SAAS,EAC3B,OAAO,EAAK,IAAI,IAAO,GAUlB,SAAS,CAAQ,CAAC,EAAwB,CAC/C,OAAO,KAAK,UAAU,EAAO,CAAC,EAAc,IAAkB,CAC5D,GAAI,OAAO,IAAS,UAAY,IAAS,KACvC,OAAO,EAET,GAAI,KAAa,EACf,MAAU,UAAU,kEAAkE,EAGxF,GAAI,aAAgB,aAAe,YAAY,OAAO,CAAI,EACxD,MAAU,UAAU,iEAAiE,EAEvF,OAAO,EACR,ECrHI,MAAM,UAAqB,KAAM,CAG3B,OAFX,WAAW,CACT,EACS,EACT,CACA,MAAM,CAAO,EAFJ,cAGT,KAAK,KAAO,eAEhB,CAEO,SAAS,CAAM,CAAC,EAAa,EAAuB,CACzD,OAAO,EAAW,EAAK,CAAE,OAAQ,KAAM,EAAG,CAAI,EAGzC,SAAS,CAAO,CAAC,EAAa,EAAkB,EAAuB,CAC5E,OAAO,EAAW,EAAK,CAAE,OAAQ,OAAQ,KAAM,EAAS,CAAO,CAAE,EAAG,CAAI,EAGnE,SAAS,CAAQ,CAAC,EAAa,EAAkB,EAAuB,CAC7E,OAAO,EAAW,EAAK,CAAE,OAAQ,QAAS,KAAM,EAAS,CAAO,CAAE,EAAG,CAAI,EAGpE,SAAS,CAAM,CAAC,EAAa,EAAkB,EAAuB,CAC3E,OAAO,EAAW,EAAK,CAAE,OAAQ,MAAO,KAAM,EAAS,CAAO,CAAE,EAAG,CAAI,EAGlE,SAAS,CAAS,CAAC,EAAa,EAAuB,CAC5D,OAAO,EAAW,EAAK,CAAE,OAAQ,QAAS,EAAG,CAAI,EAQ5C,SAAS,CAAQ,CAAC,EAAa,EAAkB,EAAuB,CAC7E,OAAO,EAAW,EAAK,CAAE,OAAQ,QAAS,KAAM,EAAS,CAAO,CAAE,EAAG,CAAI,EAG3E,SAAS,CAAU,CAAC,EAAa,EAAmB,EAAuB,CAQzE,GAPA,EAAO,CAAE,MAAO,QAAS,MAAK,CAAC,EAE/B,EAAK,QAAU,CACb,OAAQ,mBACR,eAAgB,sBACb,GAAM,OACX,EACI,GAAM,OACR,EAAK,OAAS,EAAK,OAGrB,OAAO,MAAM,EAAK,CAAI,EACnB,KAAK,CAAC,IACL,EAAQ,KAAK,EAAE,KAAK,CAAC,IAAkB,CAErC,GADkB,EAAQ,QAAU,KAAO,EAAQ,OAAS,IAG1D,OADA,EAAO,CAAE,MAAO,UAAW,MAAK,CAAC,EAC1B,EAET,IAAM,EAAY,EACZ,EAAQ,CACZ,QAAS,GAAW,OAAO,SAAW,EAAQ,WAC9C,KAAM,GAAW,OAAO,MAAQ,EAAQ,MAC1C,EAEA,MADA,EAAO,CAAE,MAAO,QAAS,QAAO,MAAK,CAAC,EAChC,IAAI,EAAa,EAAM,QAAS,EAAM,IAAI,EACjD,CACH,EACC,QAAQ,IAAM,CACb,EAAO,CAAE,MAAO,WAAY,MAAK,CAAC,EACnC,EChFE,SAAS,CAAS,CAAC,EAAqB,CAC7C,IAAI,EAAO,EAAI,OAAO,CAAC,EAAE,YAAY,EACrC,QAAS,EAAI,EAAG,EAAI,EAAI,OAAQ,EAAE,EAChC,GAAQ,EAAI,KAAO,EAAI,GAAG,YAAY,EAAI,IAAM,EAAI,GAAG,YAAY,EAAI,EAAI,GAE7E,OAAO,ECWF,IAAM,EAAc,CACzB,SAAU,CAAE,OAAQ,MAAO,KAAM,EAAG,EACpC,QAAS,CAAE,OAAQ,MAAO,KAAM,MAAO,EACvC,MAAO,CAAE,OAAQ,MAAO,KAAM,QAAS,EACvC,YAAa,CAAE,OAAQ,MAAO,KAAM,MAAO,EAC3C,UAAW,CAAE,OAAQ,OAAQ,KAAM,EAAG,EACtC,WAAY,CAAE,OAAQ,OAAQ,KAAM,OAAQ,EAC5C,QAAS,CAAE,OAAQ,MAAO,KAAM,EAAG,EACnC,SAAU,CAAE,OAAQ,MAAO,KAAM,OAAQ,EACzC,WAAY,CAAE,OAAQ,QAAS,KAAM,EAAG,EACxC,cAAe,CAAE,OAAQ,QAAS,KAAM,MAAO,EAC/C,cAAe,CAAE,OAAQ,SAAU,KAAM,MAAO,EAChD,WAAY,CAAE,OAAQ,SAAU,KAAM,EAAG,CAC3C,EAaM,EAAW,EAAQ,CAAW,EAG9B,GAAqD,IAAI,IAC7D,EAAS,OAAO,CAAC,IAAO,EAAY,GAAI,SAAW,OAAS,EAAY,GAAI,OAAS,MAAM,EAAE,IAAI,CAAC,IAAO,CACvG,EAAY,GAAI,KAChB,CACF,CAAC,CACH,EAKO,SAAS,CAAa,CAAC,EAAyB,CACrD,OAAO,EAAU,EAAO,IAAI,ECV9B,SAAS,CAAY,CAAC,EAAiB,EAAyB,CAC9D,GAAI,CAAC,EAAW,CAAE,EAChB,MAAU,UAAU,IAAI,EAAO,yEAAyE,EAE1G,OAAO,OAAO,CAAE,EAGX,MAAM,CAAqC,CAErC,SACA,SAFX,WAAW,CACA,EACA,EAAgC,CAAC,EAC1C,CAFS,gBACA,qBAGL,YAOL,CACC,EACA,EACA,EACA,EACgF,CAChF,IAAM,EAAW,KAAK,YAAY,CAAM,EAClC,EAAK,EAAe,CAAC,EAC3B,OAAO,EACL,GAAG,KAAY,EAAU,EAAQ,CAAE,IAAI,IACvC,KAAK,aAAa,CAAI,CACxB,EAGF,OAOC,CACC,EACA,EACA,EACgF,CAChF,OAAO,KAAK,KACV,GAAG,KAAK,YAAY,CAAM,IAAI,EAAY,QAAQ,OAClD,EACA,CACF,EAGF,QAOC,CACC,EACA,EACA,EACsE,CACtE,IAAM,EAA2C,IAAK,CAAE,EACxD,GAAI,GAAM,MACR,EAAK,MAAQ,GAEf,OAAO,KAAK,KAA0C,KAAK,YAAY,CAAM,EAAG,EAAM,CAAI,OAGtF,iBAOL,CACC,EACA,EACA,EAC6E,CAC7E,IAAM,EAAW,MAAM,KAAK,SAAS,EAAQ,EAAG,IAAK,EAAM,MAAO,EAAK,CAAC,EACxE,GAAI,OAAO,EAAS,QAAU,SAC5B,MAAU,UAAU,gDAAgD,EAEtE,MAAO,IAAK,EAAU,MAAO,EAAS,KAAM,EAG9C,KAAuB,CAAC,EAAiB,EAAyB,EAAuB,CACvF,OAAO,KAAK,KAAa,GAAG,KAAK,YAAY,CAAM,IAAI,EAAY,MAAM,OAAQ,EAAG,CAAI,OAIpF,OAAwB,CAAC,EAAiB,EAA2B,EAAuB,CAChG,IAAM,EAAM,MAAM,KAAK,MAAM,EAAQ,IAAK,EAAG,OAAQ,CAAE,EAAG,CAAI,EAC9D,MAAO,IAAK,EAAK,KAAM,EAAI,KAAO,CAAE,EAGtC,SAA2B,CAAC,EAAiB,EAAyB,EAAuB,CAC3F,IAAM,EAAW,KAAK,YAAY,CAAM,EACxC,OAAO,EAA+B,EAAU,EAAS,KAAK,aAAa,CAAI,CAAC,EAGlF,UAA4B,CAAC,EAAiB,EAA2B,EAAuB,CAC9F,IAAM,EAAW,KAAK,YAAY,CAAM,EACxC,OAAO,EACL,GAAG,IAAW,EAAY,WAAW,OACrC,EACA,KAAK,aAAa,CAAI,CACxB,OAGI,cAA+B,CACnC,EACA,EACA,EACA,EACA,CACA,IAAM,EAAW,KAAK,YAAY,CAAM,EACxC,OAAO,EAAc,GAAG,KAAY,EAAU,EAAQ,CAAE,IAAK,EAAS,KAAK,aAAa,CAAI,CAAC,EAG/F,UAA4B,CAC1B,EACA,EACA,EACA,EACA,CACA,IAAM,EAAW,KAAK,YAAY,CAAM,EAClC,EAAK,EAAe,CAAC,EAC3B,OAAO,EAAc,GAAG,IAAW,IAAM,EAAS,KAAK,aAAa,CAAI,CAAC,EAG3E,OAAyB,CAAC,EAAiB,EAAyB,EAAuB,CACzF,IAAM,EAAW,KAAK,YAAY,CAAM,EACxC,OAAO,EAA8B,EAAU,EAAS,KAAK,aAAa,CAAI,CAAC,EAGjF,QAA0B,CAAC,EAAiB,EAA2B,EAAuB,CAC5F,IAAM,EAAW,KAAK,YAAY,CAAM,EACxC,OAAO,EACL,GAAG,IAAW,EAAY,SAAS,OACnC,EACA,KAAK,aAAa,CAAI,CACxB,OAGI,cAA+B,CAAC,EAAiB,EAAiB,EAAsC,CAAC,EAAG,CAChH,IAAM,EAAW,KAAK,YAAY,CAAM,EAClC,EAAK,EAAK,WAAa,EAAe,CAAE,WAAY,EAAK,UAAW,CAAC,EAAI,GAC/E,OAAO,EAAe,GAAG,KAAY,EAAU,EAAQ,CAAE,IAAI,IAAM,KAAK,aAAa,CAAI,CAAC,EAG5F,UAA4B,CAAC,EAAiB,EAA0B,EAAsC,CAAC,EAAG,CAChH,IAAM,EAAW,KAAK,YAAY,CAAM,EAClC,EAAK,EAAe,EAAK,WAAa,IAAK,EAAG,WAAY,EAAK,UAAW,EAAI,CAAC,EACrF,OAAO,EAAe,GAAG,IAAW,IAAM,KAAK,aAAa,CAAI,CAAC,EAGnE,WAAc,CAAC,EAAiB,CAC9B,MAAO,GAAG,KAAK,aAAa,KAAK,SAAS,YAAc,GAAY,CAAM,IAGlE,IAAO,CAAC,EAAc,EAAwC,EAAuB,CAC7F,GAAI,KAAK,SAAS,aAAe,QAC/B,OAAO,EAAa,EAAM,GAAK,CAAC,EAAG,KAAK,aAAa,CAAI,CAAC,EAE5D,OAAO,EAAO,GAAG,IAAO,EAAe,CAAC,IAAK,KAAK,aAAa,CAAI,CAAC,EAG5D,YAAY,CAAC,EAAmD,CACxE,GAAI,CAAC,KAAK,SAAS,SAAW,CAAC,GAAM,QACnC,OAAO,EAET,MAAO,IAAK,EAAM,QAAS,IAAK,KAAK,SAAS,WAAY,GAAM,OAAQ,CAAE,EAE9E,CC9NA,IAAI,EAAiC,CACnC,WAAY,IAAM,IAAI,EAAY,MAAM,CAC1C,EAEO,SAAS,EAA2C,CAAC,EAAS,CACnE,EAAc,EAGT,SAAS,CAAc,EAAsB,CAClD,OAAO,EAGF,SAAS,EAAU,EAAkB,CAC1C,OAAO,EAAe,EAAE,WAAW",
17
+ "debugId": "948F02FA588B6B3064756E2164756E21",
18
18
  "names": []
19
19
  }
@@ -1,4 +1,4 @@
1
- import { type ColumnFamily, type EntityData, type EntityMeta, type EntityWhereMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, type QueryBuildFn, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryContextOptions, type QueryExclude, type QueryGroupMap, type QueryGroupOp, type QueryHavingMap, type QueryOptions, type QueryPage, type QueryPager, QueryRaw, type QueryRawFnOptions, type QuerySearch, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryWhere, type QueryWhereArray, type QueryWhereOptions, type RelationAggregateSpec, type RelationMeta, type SqlDialectName, type SqlQueryDialect, type Type, type UpdatePayload } from '../type/index.js';
1
+ import { type ColumnFamily, type EntityData, type EntityMeta, type EntityWhereMeta, type FieldKey, type FieldOptions, type IsolationLevel, type JsonColumnType, type JsonUpdateOp, type Query, type QueryAggMap, type QueryAggregate, type QueryBuildFn, type QueryComparisonOptions, type QueryConflictPaths, type QueryContext, type QueryContextOptions, type QueryExclude, type QueryGroupMap, type QueryGroupOp, type QueryHavingMap, type QueryOptions, type QueryPage, type QueryPager, QueryRaw, type QueryRawFnOptions, type QuerySearch, type QuerySelectValue, type QuerySizeComparisonOps, type QueryTextSearchOptions, type QueryWhere, type QueryWhereArray, type QueryWhereOptions, type RelationAggregateSpec, type RelationMeta, type SqlDialectName, type SqlQueryDialect, type Type, type UpdatePayload } from '../type/index.js';
2
2
  import type { HydrateKind } from './hydrateColumn.js';
3
3
  import { type JsonAccessMode, type JsonSlot } from './jsonSql.js';
4
4
  import { type QueryJoins, type QuerySortOptions } from './queryJoins.js';
@@ -182,6 +182,13 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
182
182
  * than inheriting another engine's syntax.
183
183
  */
184
184
  protected appendTextSearch<E>(_ctx: QueryContext, _entity: Type<E>, _meta: EntityMeta<E>, _search: QueryTextSearchOptions<E>): void;
185
+ /**
186
+ * A row's relevance to a `$text` search, higher for a better match: what `$sort: { $text }` orders by.
187
+ * Each engine that searches scores too, so a dialect overrides this beside {@link appendTextSearch}.
188
+ */
189
+ protected appendTextRank<E>(_ctx: QueryContext, _meta: EntityMeta<E>, _search: QueryTextSearchOptions<E>): void;
190
+ /** Ranks by the root `$text` of `where`, which is looked up only once a `$sort` asks for it. */
191
+ private textRanker;
185
192
  select<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>, opts?: ReadOptions, joins?: QueryJoins, totalAlias?: string): ReadProjection;
186
193
  /**
187
194
  * Everything a read's rows answer under, in order: its own fields, each joined row's fields and to-many
@@ -345,7 +352,7 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
345
352
  getComparisonKey<E>(ctx: QueryContext, entity: Type<E>, key: FieldKey<E>, opts?: QueryOptions): void;
346
353
  /** Appends the `ORDER BY`, reporting whether there was one - which {@link pager} needs on the
347
354
  * engines that refuse to page an unordered statement. */
348
- sort<E>(ctx: QueryContext, entity: Type<E>, sort: QuerySortMap<E> | undefined, opts?: QuerySortOptions): boolean;
355
+ sort<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>, opts?: QuerySortOptions): boolean;
349
356
  /**
350
357
  * The terms of an `ORDER BY`, collected before anything is appended so an unorderable key is reported
351
358
  * instead of half a clause, and because a vector distance is the primary ordering wherever it appears.
@@ -1,7 +1,7 @@
1
1
  import { fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
2
2
  import { COUNT_RESULT_KEY, parseQueryLock, QueryRaw, RAW_ALIAS, VECTOR_QUERY_KEYS, } from '../type/index.js';
3
3
  import { isInlinedExpression } from '../util/field.util.js';
4
- import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, targetKeyColumns, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, populatesRelations, aggregateOf, raw, refs, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
4
+ import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, fieldUpdateOf, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, rankedTextSearch, targetKeyColumns, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, populatesRelations, aggregateOf, raw, refs, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
5
5
  import { escapeAnsiSqlLiteral } from '../util/sqlLiteral.js';
6
6
  import { AGGREGATE_PAGE_ALIAS, AGGREGATE_VALUE_ALIAS, ROWS_ALIAS, JSON_ELEM_ALIAS, JSON_PULL_ALIAS, relationSortColumn, } from './aliases.js';
7
7
  import { holdsOperator, isJsonScalar, jsonCompareMode, jsonElemExists, jsonPath, } from './jsonSql.js';
@@ -9,6 +9,8 @@ import { SqlQueryContext } from './queryContext.js';
9
9
  import { groupPathField, NO_JOINS, resolveGroupJoins, resolveQueryJoins, resolveSortableJoin, } from './queryJoins.js';
10
10
  import { resolveVectorCast } from './vectorCast.js';
11
11
  import { VectorSqlDialect } from './vectorSqlDialect.js';
12
+ /** A scalar field's operator as the SQL arithmetic computing it. */
13
+ const SQL_ARITHMETIC = { $inc: '+', $mul: '*' };
12
14
  /** The key a term answers under in a populated relation's row, which a raw expression has only once aliased. */
13
15
  export function relationTermKey({ sql, key }) {
14
16
  return orRefuse(key, `a raw $select in a populated relation needs an alias, the key its value lands under: ${sql}`);
@@ -151,9 +153,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
151
153
  opts = { ...opts, prefix };
152
154
  }
153
155
  this.where(ctx, entity, this.rankedWhere(meta, q, prefix), opts);
154
- const sorted = order
155
- ? this.orderCarried(ctx, q, order)
156
- : this.sort(ctx, entity, q.$sort, { prefix, joins, distinct: q.$distinct });
156
+ const sorted = order ? this.orderCarried(ctx, q, order) : this.sort(ctx, entity, q, { prefix, joins });
157
157
  this.pager(ctx, q, sorted);
158
158
  }
159
159
  /**
@@ -224,12 +224,23 @@ export class AbstractSqlDialect extends VectorSqlDialect {
224
224
  appendTextSearch(_ctx, _entity, _meta, _search) {
225
225
  throw new TypeError(`${this.dialectName} does not support $text full-text search`);
226
226
  }
227
+ /**
228
+ * A row's relevance to a `$text` search, higher for a better match: what `$sort: { $text }` orders by.
229
+ * Each engine that searches scores too, so a dialect overrides this beside {@link appendTextSearch}.
230
+ */
231
+ appendTextRank(_ctx, _meta, _search) {
232
+ throw new TypeError(`${this.dialectName} does not support $text full-text search`);
233
+ }
234
+ /** Ranks by the root `$text` of `where`, which is looked up only once a `$sort` asks for it. */
235
+ textRanker(meta, where) {
236
+ return (ctx) => this.appendTextRank(ctx, meta, rankedTextSearch(where));
237
+ }
227
238
  select(ctx, entity, q, opts = {}, joins = NO_JOINS, totalAlias) {
228
239
  const meta = getMeta(entity);
229
240
  const { alias, ref } = this.tableRef(meta, opts.alias);
230
241
  const prefix = this.resolveRelationAwarePrefix(alias, meta, opts, q.$populate, joins);
231
242
  const terms = this.projection(ctx, entity, q, { prefix, json: opts.json }, joins);
232
- const carried = opts.carried ? this.carrySort(ctx, meta, q, { prefix, joins, distinct: q.$distinct }) : undefined;
243
+ const carried = opts.carried ? this.carrySort(ctx, meta, q, { prefix, joins }) : undefined;
233
244
  const columns = carried ? [...terms, ...carried.columns] : [...terms];
234
245
  if (totalAlias) {
235
246
  columns.push({ sql: this.totalOverExpr, key: totalAlias });
@@ -279,7 +290,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
279
290
  */
280
291
  carrySort(ctx, meta, q, opts) {
281
292
  const columns = [];
282
- const order = this.sortTerms(ctx, meta, q.$sort, opts).map(({ key, expr, direction, output }) => {
293
+ const order = this.sortTerms(ctx, meta, q, opts).map(({ key, expr, direction, output }) => {
283
294
  if (output) {
284
295
  return { ref: expr, direction };
285
296
  }
@@ -753,8 +764,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
753
764
  }
754
765
  /** Appends the `ORDER BY`, reporting whether there was one - which {@link pager} needs on the
755
766
  * engines that refuse to page an unordered statement. */
756
- sort(ctx, entity, sort, opts = {}) {
757
- const terms = this.sortTerms(ctx, getMeta(entity), sort, opts);
767
+ sort(ctx, entity, q, opts = {}) {
768
+ const terms = this.sortTerms(ctx, getMeta(entity), q, opts);
758
769
  if (terms.length) {
759
770
  ctx.append(` ORDER BY ${terms.map(({ expr, direction }) => expr + direction).join(', ')}`);
760
771
  }
@@ -764,13 +775,14 @@ export class AbstractSqlDialect extends VectorSqlDialect {
764
775
  * The terms of an `ORDER BY`, collected before anything is appended so an unorderable key is reported
765
776
  * instead of half a clause, and because a vector distance is the primary ordering wherever it appears.
766
777
  */
767
- sortTerms(ctx, meta, sort, opts) {
768
- if (!hasKeys(sort)) {
778
+ sortTerms(ctx, meta, q, opts) {
779
+ if (!hasKeys(q.$sort)) {
769
780
  return [];
770
781
  }
771
782
  const vectors = [];
772
783
  const columns = [];
773
- this.collectSortTerms(ctx, meta, sort, opts, vectors, columns);
784
+ const walk = { ...opts, distinct: q.$distinct, rankText: this.textRanker(meta, q.$where) };
785
+ this.collectSortTerms(ctx, meta, q.$sort, walk, vectors, columns);
774
786
  return [...vectors, ...columns];
775
787
  }
776
788
  /**
@@ -785,6 +797,14 @@ export class AbstractSqlDialect extends VectorSqlDialect {
785
797
  for (const [key, value] of Object.entries(sort)) {
786
798
  const relation = meta.relations[key];
787
799
  const keyPath = path ? `${path}.${key}` : key;
800
+ if (key === '$text') {
801
+ if (path) {
802
+ throw new TypeError(`$sort by $text is only supported on the queried entity, not on relation '${path}'`);
803
+ }
804
+ const expr = this.buildFragment(ctx, opts.rankText);
805
+ columns.push({ key, expr, direction: this.resolveSortDirection(value), output: false });
806
+ continue;
807
+ }
788
808
  if (relation) {
789
809
  const countDirection = parseSortByCount(value);
790
810
  if (countDirection !== undefined) {
@@ -1218,6 +1238,11 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1218
1238
  if (isJsonUpdateOp(value)) {
1219
1239
  this.formatJsonUpdate(ctx, escapedCol, value, field);
1220
1240
  }
1241
+ else if (isFieldUpdateOp(value)) {
1242
+ const [op, operand] = fieldUpdateOf(value);
1243
+ ctx.append(`${escapedCol} = COALESCE(${escapedCol}, 0) ${SQL_ARITHMETIC[op]} `);
1244
+ ctx.addValue(operand);
1245
+ }
1221
1246
  else {
1222
1247
  ctx.append(`${escapedCol} = `);
1223
1248
  this.formatPersistableValue(ctx, field, value);
@@ -21,6 +21,8 @@ export declare const RELATION_ROW_ALIAS = "_uql_row";
21
21
  export declare const JSON_ELEM_ALIAS = "_uql_elem";
22
22
  /** The alias a `$pull` reads its surviving elements through, kept distinct from {@link JSON_ELEM_ALIAS}. */
23
23
  export declare const JSON_PULL_ALIAS = "_uql_pull";
24
+ /** The key a MongoDB `$sort` by `$text` orders `textScore` under, which names no field of the document. */
25
+ export declare const TEXT_SCORE_ALIAS = "_uql_text_score";
24
26
  /** Prefix for the field a MongoDB relation lookup parks its result on, one per condition. */
25
27
  export declare const REL_TEMP_PREFIX = "_uql_rel_";
26
28
  /** The field a ManyToMany lookup nests its target match under, inside the junction's own pipeline. */
@@ -23,6 +23,8 @@ export const RELATION_ROW_ALIAS = '_uql_row';
23
23
  export const JSON_ELEM_ALIAS = '_uql_elem';
24
24
  /** The alias a `$pull` reads its surviving elements through, kept distinct from {@link JSON_ELEM_ALIAS}. */
25
25
  export const JSON_PULL_ALIAS = '_uql_pull';
26
+ /** The key a MongoDB `$sort` by `$text` orders `textScore` under, which names no field of the document. */
27
+ export const TEXT_SCORE_ALIAS = '_uql_text_score';
26
28
  /** Prefix for the field a MongoDB relation lookup parks its result on, one per condition. */
27
29
  export const REL_TEMP_PREFIX = '_uql_rel_';
28
30
  /** The field a ManyToMany lookup nests its target match under, inside the junction's own pipeline. */
@@ -80,6 +80,8 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
80
80
  * `@Index((post) => [...], { type: 'fulltext' })`.
81
81
  */
82
82
  protected appendTextSearch<E>(ctx: QueryContext, _entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
83
+ /** `MATCH ... AGAINST` is the relevance itself, a match being any row it scores above zero. */
84
+ protected appendTextRank<E>(ctx: QueryContext, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
83
85
  /** `DOUBLE`, never a bare `DECIMAL`, which is `DECIMAL(10,0)` and rounds `1.4` to `1`. */
84
86
  protected numericCast(expr: string): string;
85
87
  protected neExpr(field: string, ph: string): string;
@@ -178,6 +178,10 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
178
178
  * `@Index((post) => [...], { type: 'fulltext' })`.
179
179
  */
180
180
  appendTextSearch(ctx, _entity, meta, search) {
181
+ this.appendTextRank(ctx, meta, search);
182
+ }
183
+ /** `MATCH ... AGAINST` is the relevance itself, a match being any row it scores above zero. */
184
+ appendTextRank(ctx, meta, search) {
181
185
  const columns = textSearchFields(meta, search).map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])));
182
186
  ctx.append(`MATCH(${this.textSearchTarget(columns)}) AGAINST(`);
183
187
  ctx.addValue(search.$value);
@@ -64,6 +64,10 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
64
64
  * input (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `TO_TSQUERY`.
65
65
  */
66
66
  protected appendTextSearch<E>(ctx: QueryContext, _entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
67
+ /** `TS_RANK` of the document the predicate matches, against the same search. */
68
+ protected appendTextRank<E>(ctx: QueryContext, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
69
+ /** The document a search reads and the search itself, open for its value. */
70
+ private textSearchParts;
67
71
  /**
68
72
  * The document, `TO_TSVECTOR('english'::regconfig, COALESCE("a", '') || ' ' || COALESCE("b", ''))`, a
69
73
  * `NULL` column read as empty rather than emptying it all. The config is a literal: an index is built
@@ -129,13 +129,28 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
129
129
  * input (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `TO_TSQUERY`.
130
130
  */
131
131
  appendTextSearch(ctx, _entity, meta, search) {
132
+ const { document, query } = this.textSearchParts(meta, search);
133
+ ctx.append(`${document} @@ ${query}`);
134
+ ctx.addValue(search.$value);
135
+ ctx.append(')');
136
+ }
137
+ /** `TS_RANK` of the document the predicate matches, against the same search. */
138
+ appendTextRank(ctx, meta, search) {
139
+ const { document, query } = this.textSearchParts(meta, search);
140
+ ctx.append(`TS_RANK(${document}, ${query}`);
141
+ ctx.addValue(search.$value);
142
+ ctx.append('))');
143
+ }
144
+ /** The document a search reads and the search itself, open for its value. */
145
+ textSearchParts(meta, search) {
132
146
  const keys = textSearchFields(meta, search);
133
147
  const index = fulltextIndexOver(meta, keys);
134
148
  const config = search.$config ?? (index && fulltextConfig(index));
135
149
  const columns = keys.map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])));
136
- ctx.append(`${this.textSearchTarget(columns, config)} @@ ${this.textQueryFn}(${this.textConfigArg(config)}`);
137
- ctx.addValue(search.$value);
138
- ctx.append(')');
150
+ return {
151
+ document: this.textSearchTarget(columns, config),
152
+ query: `${this.textQueryFn}(${this.textConfigArg(config)}`,
153
+ };
139
154
  }
140
155
  /**
141
156
  * The document, `TO_TSVECTOR('english'::regconfig, COALESCE("a", '') || ' ' || COALESCE("b", ''))`, a
@@ -32,7 +32,6 @@ export type QuerySortOptions = {
32
32
  /** Alias the queried entity's own columns are qualified by, when the statement qualifies them. */
33
33
  readonly prefix?: string;
34
34
  readonly joins?: QueryJoins;
35
- readonly distinct?: boolean;
36
35
  };
37
36
  /**
38
37
  * What the statement joins, from `$populate` and from a `$sort` by a to-one relation's field, so the
@@ -27,8 +27,7 @@ export class MariaDialect extends MysqlLikeSqlDialect {
27
27
  appendRelationArray(ctx, { entity, query, alias, joins }) {
28
28
  const meta = getMeta(entity);
29
29
  const terms = this.projection(ctx, entity, query, { prefix: alias, json: true }, joins);
30
- const sortOpts = { prefix: alias, joins, distinct: query.$distinct };
31
- const order = this.buildFragment(ctx, (fragmentCtx) => this.sort(fragmentCtx, entity, query.$sort, sortOpts));
30
+ const order = this.buildFragment(ctx, (fragmentCtx) => this.sort(fragmentCtx, entity, query, { prefix: alias, joins }));
32
31
  const page = this.buildFragment(ctx, (fragmentCtx) => this.pager(fragmentCtx, query));
33
32
  const from = this.buildFragment(ctx, (fragmentCtx) => {
34
33
  this.selectRelationJoins(fragmentCtx, meta, alias, joins);
@@ -1,6 +1,6 @@
1
1
  import { type Document, type Filter, type Sort, type UpdateFilter } from 'mongodb';
2
2
  import { AbstractDialect } from '../dialect/abstractDialect.js';
3
- import type { DialectFeatures, EntityData, EntityMeta, Query, QueryAggMap, QueryAggregate, QueryExclude, QueryGroupMap, QueryOptions, QueryPager, QueryPopulate, QuerySelectValue, QuerySortMap, QueryVectorSearch, QueryWhere, Type } from '../type/index.js';
3
+ import type { DialectFeatures, EntityData, EntityMeta, Query, QueryAggMap, QueryAggregate, QueryExclude, QueryGroupMap, QueryOptions, QueryPager, QuerySelectValue, QuerySortMap, QueryVectorSearch, QueryWhere, Type } from '../type/index.js';
4
4
  import { type CallbackKey } from '../util/index.js';
5
5
  /** What a read pipeline contributes to {@link MongoDialect.readStages} beyond the query itself. */
6
6
  type MongoReadStages = {
@@ -122,7 +122,7 @@ export declare class MongoDialect extends AbstractDialect {
122
122
  * means a *populated* one, at every level of the path: a lookup adds a field to the result, so one
123
123
  * added for the sort alone would change what the caller gets back.
124
124
  */
125
- sort<E extends Document>(entity: Type<E>, sort?: QuerySortMap<E>, populate?: QueryPopulate<E>): Sort;
125
+ sort<E extends Document>(entity: Type<E>, { $sort: sort, $populate: populate, $where: where }: Query<E>): Sort;
126
126
  /** Walks `$sort` against the metadata of the entity each level addresses, as the SQL dialects do. */
127
127
  private collectSort;
128
128
  /**
@@ -237,7 +237,7 @@ export declare class MongoDialect extends AbstractDialect {
237
237
  */
238
238
  getUpdateFilter<E extends Document>(persistable: Partial<E>): UpdateFilter<E> | Document[];
239
239
  /**
240
- * A JSON update as one pipeline, since MongoDB refuses two operators on one path: each path composed as
240
+ * An update as one pipeline, since MongoDB refuses two operators on one path: each path composed as
241
241
  * `$pull`, `$set`, `$push`, then `$unset`, as SQL does, with values as `$literal` so `$x` stays data.
242
242
  */
243
243
  private getUpdatePipeline;
@@ -1,11 +1,13 @@
1
1
  import { ObjectId } from 'mongodb';
2
2
  import { AbstractDialect } from '../dialect/abstractDialect.js';
3
- import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, sortCountField, } from '../dialect/aliases.js';
3
+ import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, sortCountField, TEXT_SCORE_ALIAS, } from '../dialect/aliases.js';
4
4
  import { groupPathField, resolveGroupJoins, resolveQueryJoins, resolveSortableJoin, } from '../dialect/queryJoins.js';
5
5
  import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
6
6
  import { COUNT_RESULT_KEY } from '../type/query.js';
7
7
  import { QueryRaw } from '../type/queryRaw.js';
8
- import { aggregateOf, asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
8
+ import { aggregateOf, asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fieldUpdateOf, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, rankedTextSearch, someKey, targetKeyColumns, } from '../util/index.js';
9
+ /** A scalar field's operator as the aggregation operator computing it. */
10
+ const MONGO_ARITHMETIC = { $inc: '$add', $mul: '$multiply' };
9
11
  /** Default {@link DialectFeatures} for MongoDB. */
10
12
  export const mongoDialectFeatures = {
11
13
  ifNotExists: false,
@@ -454,9 +456,13 @@ export class MongoDialect extends AbstractDialect {
454
456
  * means a *populated* one, at every level of the path: a lookup adds a field to the result, so one
455
457
  * added for the sort alone would change what the caller gets back.
456
458
  */
457
- sort(entity, sort, populate) {
459
+ sort(entity, { $sort: sort, $populate: populate, $where: where }) {
458
460
  const meta = getMeta(entity);
459
461
  const normalized = {};
462
+ // Refused as the SQL dialects refuse it, before MongoDB answers a missing score with its own error.
463
+ if (sort?.$text) {
464
+ rankedTextSearch(where);
465
+ }
460
466
  // The same join set the lookups are built from, so what an ordering may address and what the
461
467
  // pipeline actually produces cannot drift apart - `$sort` contributes its own to-one joins here
462
468
  // exactly as it does on the SQL dialects.
@@ -467,6 +473,13 @@ export class MongoDialect extends AbstractDialect {
467
473
  collectSort(meta, sort, joins, path, out) {
468
474
  for (const [key, value] of Object.entries(sort ?? {})) {
469
475
  const relation = meta.relations[key];
476
+ if (key === '$text') {
477
+ if (path) {
478
+ throw new TypeError(`$sort by $text is only supported on the queried entity, not on relation '${path.slice(0, -1)}'`);
479
+ }
480
+ out[TEXT_SCORE_ALIAS] = { $meta: 'textScore' };
481
+ continue;
482
+ }
470
483
  if (!relation) {
471
484
  // The queried entity's own vector search is lifted out before this walk, so one reaching it
472
485
  // sits under a relation, which a `$lookup` brings in one row at a time - there is nothing to
@@ -553,7 +566,7 @@ export class MongoDialect extends AbstractDialect {
553
566
  const relOpts = relationOf(meta, spec.relation);
554
567
  const query = spec.query ?? {};
555
568
  const tail = [
556
- ...(query.$sort ? [{ $sort: this.sort(relOpts.entity(), query.$sort) }] : []),
569
+ ...(query.$sort ? [{ $sort: this.sort(relOpts.entity(), query) }] : []),
557
570
  ...this.pagerStages(query),
558
571
  spec.field
559
572
  ? {
@@ -669,7 +682,7 @@ export class MongoDialect extends AbstractDialect {
669
682
  return [
670
683
  ...this.matchStages(entity, q.$where, opts, this.aggregateKeys(entity, q)),
671
684
  ...this.readStages(entity, q, {
672
- sort: this.sort(entity, q.$sort, q.$populate),
685
+ sort: this.sort(entity, q),
673
686
  pager: this.pagerStages(q),
674
687
  }),
675
688
  ];
@@ -912,8 +925,14 @@ export class MongoDialect extends AbstractDialect {
912
925
  const set = {};
913
926
  const push = {};
914
927
  const pull = {};
928
+ const arithmetic = {};
915
929
  const unset = new Set();
916
930
  for (const [key, value] of Object.entries(persistable)) {
931
+ if (isFieldUpdateOp(value)) {
932
+ const [op, operand] = fieldUpdateOf(value);
933
+ arithmetic[key] = { [MONGO_ARITHMETIC[op]]: [{ $ifNull: [`$${key}`, 0] }, { $literal: operand }] };
934
+ continue;
935
+ }
917
936
  if (!isJsonUpdateOp(value)) {
918
937
  set[key] = value;
919
938
  continue;
@@ -931,7 +950,7 @@ export class MongoDialect extends AbstractDialect {
931
950
  pull[`${key}.${path}`] = v;
932
951
  }
933
952
  }
934
- return { set, push, pull, unset };
953
+ return { set, push, pull, arithmetic, unset };
935
954
  }
936
955
  /**
937
956
  * Turn a persistable payload into a MongoDB update, mapping UQL's JSON operators onto their native
@@ -940,12 +959,12 @@ export class MongoDialect extends AbstractDialect {
940
959
  */
941
960
  getUpdateFilter(persistable) {
942
961
  const groups = this.groupUpdateOperators(persistable);
943
- const { set, push, pull, unset } = groups;
962
+ const { set, push, pull, arithmetic, unset } = groups;
944
963
  const exprKeys = [...Object.keys(pull), ...Object.keys(set), ...Object.keys(push)];
945
- // Native `$pull` fails on a value that is no array, and MongoDB rejects two operators targeting one
946
- // path in a single update document: either forces the pipeline form.
964
+ // Native `$pull` fails on a value that is no array, native `$inc`/`$mul` on a `null`, and MongoDB
965
+ // rejects two operators targeting one path in a single update document: each forces the pipeline form.
947
966
  const allPaths = [...exprKeys, ...unset];
948
- if (hasKeys(pull) || new Set(allPaths).size < allPaths.length) {
967
+ if (hasKeys(pull) || hasKeys(arithmetic) || new Set(allPaths).size < allPaths.length) {
949
968
  return this.getUpdatePipeline(groups, new Set(exprKeys));
950
969
  }
951
970
  return {
@@ -955,11 +974,11 @@ export class MongoDialect extends AbstractDialect {
955
974
  };
956
975
  }
957
976
  /**
958
- * A JSON update as one pipeline, since MongoDB refuses two operators on one path: each path composed as
977
+ * An update as one pipeline, since MongoDB refuses two operators on one path: each path composed as
959
978
  * `$pull`, `$set`, `$push`, then `$unset`, as SQL does, with values as `$literal` so `$x` stays data.
960
979
  */
961
- getUpdatePipeline({ set, push, pull, unset }, exprPaths) {
962
- const assignments = {};
980
+ getUpdatePipeline({ set, push, pull, arithmetic, unset }, exprPaths) {
981
+ const assignments = { ...arithmetic };
963
982
  for (const path of exprPaths) {
964
983
  let expr = { $ifNull: [`$${path}`, []] };
965
984
  if (path in pull) {
@@ -93,7 +93,7 @@ export class MongodbQuerier extends AbstractQuerier {
93
93
  if (hasKeys(select)) {
94
94
  cursor.project(select);
95
95
  }
96
- const sort = this.dialect.sort(entity, q.$sort, q.$populate);
96
+ const sort = this.dialect.sort(entity, q);
97
97
  if (hasKeys(sort)) {
98
98
  cursor.sort(sort);
99
99
  }
@@ -119,7 +119,7 @@ export class MongodbQuerier extends AbstractQuerier {
119
119
  ...(scoreAlias ? [{ $addFields: { [scoreAlias]: { $meta: 'vectorSearchScore' } } }] : []),
120
120
  // `$vectorSearch` has already applied `$limit`, so the pager is its own.
121
121
  ...this.dialect.readStages(entity, q, {
122
- sort: this.dialect.sort(entity, vectorSort.regularSort, q.$populate),
122
+ sort: this.dialect.sort(entity, { ...q, $sort: vectorSort.regularSort }),
123
123
  project: scoreAlias ? { [scoreAlias]: 1 } : undefined,
124
124
  }),
125
125
  ];
@@ -189,7 +189,6 @@ export class MongodbQuerier extends AbstractQuerier {
189
189
  return this.timed('internalUpdateMany', undefined, async () => {
190
190
  const persistable = this.dialect.getPersistable(getMeta(entity), payload, 'onUpdate');
191
191
  const filter = this.dialect.where(entity, qm.$where, opts);
192
- // Maps JSON operators ($set/$unset/$push/$pull) onto their native MongoDB equivalents.
193
192
  const update = this.dialect.getUpdateFilter(persistable);
194
193
  const { matchedCount } = await this.execute((session) => this.collection(entity).updateMany(filter, update, { session }));
195
194
  return matchedCount;
@@ -67,6 +67,8 @@ export declare class SqliteDialect extends AbstractSqlDialect {
67
67
  * FTS5 virtual table (UQL does not create those; declare it outside your entities).
68
68
  */
69
69
  protected appendTextSearch<E>(ctx: QueryContext, entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
70
+ /** FTS5's `BM25` of the match, lower for a better one, so negated to rank as every other engine does. */
71
+ protected appendTextRank<E>(ctx: QueryContext, meta: EntityMeta<E>): void;
70
72
  protected jsonLength(slot: JsonSlot): string;
71
73
  /** `JSON_EACH` walks the array at the path itself, each element's `fullkey` naming it from the column. */
72
74
  protected jsonElemFrom(slot: JsonSlot, alias: string): string;
@@ -163,6 +163,10 @@ export class SqliteDialect extends AbstractSqlDialect {
163
163
  ctx.append(`${this.escapedTableName(meta)} MATCH {${columns.join(' ')}} : `);
164
164
  ctx.addValue(search.$value);
165
165
  }
166
+ /** FTS5's `BM25` of the match, lower for a better one, so negated to rank as every other engine does. */
167
+ appendTextRank(ctx, meta) {
168
+ ctx.append(`-BM25(${this.escapedTableName(meta)})`);
169
+ }
166
170
  jsonLength(slot) {
167
171
  return `JSON_ARRAY_LENGTH(${jsonArraySlotArgs(slot, this.jsonIsArray(slot))})`;
168
172
  }
@@ -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.
@@ -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.
@@ -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,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>[];
@@ -87,6 +87,10 @@ export declare function findVectorIndex<E>(meta: EntityMeta<E>, key: string): En
87
87
  export declare function hasVectorNear(where: unknown): boolean;
88
88
  /** Type guard: checks whether an update payload value is a JSON operator object. */
89
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];
90
94
  /**
91
95
  * The `$where` naming rows by key: a bare value names the one key column (refused on a composite), a
92
96
  * composite's key map is a `$where` already, and a list is an `IN` of bare values or an OR of maps.
@@ -169,6 +173,11 @@ export declare function fulltextConfig(index: {
169
173
  }): string;
170
174
  /** The fulltext index over exactly `fields`, in order, which a search of them is served by. */
171
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>;
172
181
  /**
173
182
  * The fields a `$text` searches: those it names, or else the columns of the entity's fulltext index,
174
183
  * 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, ...(where && hasKeys(where) ? { where } : {}) });
383
+ entries.push({ kind: 'fn', alias, op, fieldRef, distinct, ...(hasKeys(where) ? { where } : {}) });
374
384
  }
375
385
  return entries;
376
386
  }
@@ -452,6 +462,16 @@ export function fulltextIndexOver(meta, fields) {
452
462
  index.columns.length === fields.length &&
453
463
  index.columns.every((entry, at) => entry.column === fields[at]));
454
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
+ }
455
475
  /**
456
476
  * The fields a `$text` searches: those it names, or else the columns of the entity's fulltext index,
457
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.71.0",
6
+ "version": "0.72.0",
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",
@@ -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` with no `$where` throw; `{ unfiltered: true }` means the whole table.
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