uql-orm 0.73.1 → 0.74.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.
@@ -1,4 +1,4 @@
1
- var p=[];function y(e){for(let r of p)r(e)}function _(e){p.push(e);let r=p.length-1;return()=>{p.splice(r,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 r=Object.getPrototypeOf(e);return r!==Object.prototype&&r!==null}var Y=new Set([...P,...E,...O,...f,...T,"hardDelete","count"]);function i(e){if(!e)return"";let r=new URLSearchParams;for(let n of c(e)){let o=e[n];if(o===void 0)continue;r.append(n,typeof o==="object"&&o!==null?u(o):String(o))}let t=r.toString();return t?`?${t}`:""}function u(e){return JSON.stringify(e,(r,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,r){super(e);this.status=r;this.name="RequestError"}}function R(e,r){return a(e,{method:"get"},r)}function h(e,r,t){return a(e,{method:"post",body:u(r)},t)}function x(e,r,t){return a(e,{method:"patch",body:u(r)},t)}function Q(e,r,t){return a(e,{method:"put",body:u(r)},t)}function g(e,r){return a(e,{method:"delete"},r)}function K(e,r,t){return a(e,{method:"QUERY",body:u(r)},t)}function a(e,r,t){if(y({phase:"start",opts:t}),r.headers={accept:"application/json","content-type":"application/json",...t?.headers},t?.signal)r.signal=t.signal;return fetch(e,r).then((n)=>n.json().then((o)=>{if(n.status>=200&&n.status<300)return y({phase:"success",opts:t}),o;let S=o,l={message:S?.error?.message??n.statusText,code:S?.error?.code??n.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 r=e.charAt(0).toLowerCase();for(let t=1;t<e.length;++t)r+=e[t]===e[t].toUpperCase()?"-"+e[t].toLowerCase():e[t];return r}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:""}},F=c(s),re=new Map(F.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,r){if(!C(r))throw TypeError(`'${e.name}' was addressed by an id object, which the HTTP route cannot carry.`);return String(r)}class m{basePath;defaults;constructor(e,r={}){this.basePath=e;this.defaults=r}async findOneById(e,r,t,n){let o=this.getBasePath(e),d=i(t);return R(`${o}/${w(e,r)}${d}`,this.buildOptions(n))}findOne(e,r,t){return this.read(`${this.getBasePath(e)}${s.findOne.path}`,r,t)}findMany(e,r,t){let n={...r};if(t?.count)n.count=!0;return this.read(this.getBasePath(e),n,t)}async findManyAndCount(e,r,t){let n=await this.findMany(e,r,{...t,count:!0});if(typeof n.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...n,count:n.count}}count(e,r,t){return this.read(`${this.getBasePath(e)}${s.count.path}`,r,t)}async exists(e,r,t){let n=await this.count(e,{...r,$limit:1},t);return{...n,data:n.data>0}}insertOne(e,r,t){let n=this.getBasePath(e);return h(n,r,this.buildOptions(t))}insertMany(e,r,t){let n=this.getBasePath(e);return h(`${n}${s.insertMany.path}`,r,this.buildOptions(t))}async updateOneById(e,r,t,n){let o=this.getBasePath(e);return x(`${o}/${w(e,r)}`,t,this.buildOptions(n))}updateMany(e,r,t,n){let o=this.getBasePath(e),d=i(r);return x(`${o}${d}`,t,this.buildOptions(n))}saveOne(e,r,t){let n=this.getBasePath(e);return Q(n,r,this.buildOptions(t))}saveMany(e,r,t){let n=this.getBasePath(e);return Q(`${n}${s.saveMany.path}`,r,this.buildOptions(t))}async deleteOneById(e,r,t={}){let n=this.getBasePath(e),o=t.hardDelete?i({hardDelete:t.hardDelete}):"";return g(`${n}/${w(e,r)}${o}`,this.buildOptions(t))}deleteMany(e,r,t={}){let n=this.getBasePath(e),o=i(t.hardDelete?{...r,hardDelete:t.hardDelete}:r);return g(`${n}${o}`,this.buildOptions(t))}getBasePath(e){return`${this.basePath}/${(this.defaults.entityPath??j)(e)}`}read(e,r,t){if(this.defaults.readMethod==="QUERY")return K(e,r??{},this.buildOptions(t));return R(`${e}${i(r)}`,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 V(){return q}function pe(){return V().getQuerier()}export{m as HttpQuerier,k as RequestError,R as get,pe as getQuerier,V 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 r of p)r(e)}function _(e){p.push(e);let r=p.length-1;return()=>{p.splice(r,1)}}var S=["$select","$populate","$exclude","$where","$sort"],E=["$count"],T=["$skip","$limit"],f=["$candidates"],O=["$distinct"],v=["$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 r=Object.getPrototypeOf(e);return r!==Object.prototype&&r!==null}var Y=new Set([...S,...E,...T,...f,...O,"hardDelete","count"]);function i(e){if(!e)return"";let r=new URLSearchParams;for(let n of c(e)){let o=e[n];if(o===void 0)continue;r.append(n,typeof o==="object"&&o!==null?u(o):String(o))}let t=r.toString();return t?`?${t}`:""}function u(e){return JSON.stringify(e,(r,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,r){super(e);this.status=r;this.name="RequestError"}}function R(e,r){return a(e,{method:"get"},r)}function h(e,r,t){return a(e,{method:"post",body:u(r)},t)}function x(e,r,t){return a(e,{method:"patch",body:u(r)},t)}function Q(e,r,t){return a(e,{method:"put",body:u(r)},t)}function g(e,r){return a(e,{method:"delete"},r)}function k(e,r,t){return a(e,{method:"QUERY",body:u(r)},t)}function a(e,r,t){if(y({phase:"start",opts:t}),r.headers={accept:"application/json","content-type":"application/json",...t?.headers},t?.signal)r.signal=t.signal;return fetch(e,r).then((n)=>n.json().then((o)=>{if(n.status>=200&&n.status<300)return y({phase:"success",opts:t}),o;let P=o,l={message:P?.error?.message??n.statusText,code:P?.error?.code??n.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 r=e.charAt(0).toLowerCase();for(let t=1;t<e.length;++t)r+=e[t]===e[t].toUpperCase()?"-"+e[t].toLowerCase():e[t];return r}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),re=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,r){if(!C(r))throw TypeError(`'${e.name}' was addressed by an id object, which the HTTP route cannot carry.`);return String(r)}class m{basePath;defaults;constructor(e,r={}){this.basePath=e;this.defaults=r}async findOneById(e,r,t,n){let o=this.getBasePath(e),d=i(t);return R(`${o}/${w(e,r)}${d}`,this.buildOptions(n))}findOne(e,r,t){return this.read(`${this.getBasePath(e)}${s.findOne.path}`,r,t)}findMany(e,r,t){let n={...r};if(t?.count)n.count=!0;return this.read(this.getBasePath(e),n,t)}async findManyAndCount(e,r,t){let n=await this.findMany(e,r,{...t,count:!0});if(typeof n.count!=="number")throw TypeError("findManyAndCount response has an invalid count");return{...n,count:n.count}}count(e,r,t){return this.read(`${this.getBasePath(e)}${s.count.path}`,r,t)}async exists(e,r,t){let n=await this.count(e,{...r,$limit:1},t);return{...n,data:n.data>0}}insertOne(e,r,t){let n=this.getBasePath(e);return h(n,r,this.buildOptions(t))}insertMany(e,r,t){let n=this.getBasePath(e);return h(`${n}${s.insertMany.path}`,r,this.buildOptions(t))}async updateOneById(e,r,t,n){let o=this.getBasePath(e);return x(`${o}/${w(e,r)}`,t,this.buildOptions(n))}updateMany(e,r,t,n){let o=this.getBasePath(e),d=i(r);return x(`${o}${d}`,t,this.buildOptions(n))}saveOne(e,r,t){let n=this.getBasePath(e);return Q(n,r,this.buildOptions(t))}saveMany(e,r,t){let n=this.getBasePath(e);return Q(`${n}${s.saveMany.path}`,r,this.buildOptions(t))}async deleteOneById(e,r,t={}){let n=this.getBasePath(e),o=t.hardDelete?i({hardDelete:t.hardDelete}):"";return g(`${n}/${w(e,r)}${o}`,this.buildOptions(t))}deleteMany(e,r,t={}){let n=this.getBasePath(e),o=i(t.hardDelete?{...r,hardDelete:t.hardDelete}:r);return g(`${n}${o}`,this.buildOptions(t))}getBasePath(e){return`${this.basePath}/${(this.defaults.entityPath??j)(e)}`}read(e,r,t){if(this.defaults.readMethod==="QUERY")return k(e,r??{},this.buildOptions(t));return R(`${e}${i(r)}`,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=F1CD14D3A7DD82FE64756E2164756E21
3
+ //# debugId=5397AE2C0FC782D564756E2164756E21
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 * Ordering by relevance to the `$text` at the root of `$where`, in either direction as any key sorts. The\n * object form also answers it under the name `$project` gives it, most relevant first unless `$order` says.\n */\nexport type QuerySortByText = {\n $text?: QuerySortDirection | { readonly $project: string; readonly $order?: QuerySortDirection };\n};\n\n/**\n * A row with the value a `$sort` projects under the name its `$project` gives - a vector's distance, or a\n * `$text` relevance - which is not inferred: `(await querier.findMany(Post, q)) as WithProjection<Post, 'score'>[]`.\n */\nexport type WithProjection<E, K extends string> = E & Record<K, number>;\n\n/**\n * A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count`, or - where `Root` says it\n * sorts the queried entity itself, not a relation's rows - a vector distance or a `$text` relevance. One\n * mapped type over the key sets: an intersection is checked once per member, which made this the costliest.\n */\nexport type QuerySortMap<E, Root 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 : Root 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 (Root 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",
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 { QueryVectorQuery, 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/** The fields of `E` a vector search can rank by. */\ntype VectorFieldKey<E> = { [P in FieldKey<E>]: NonNullable<E[P]> extends readonly number[] ? P : never }[FieldKey<E>];\n\n/**\n * Ordering parents by the row of a to-many nearest a vector, per vector field: its distance is the\n * smallest of theirs. Nothing to `$project`, since no one row of the parent's answers under it. Never\n * where the target has no vector, since an empty map would admit any value at all.\n */\nexport type QuerySortByNearest<E> = [VectorFieldKey<E>] extends [never]\n ? never\n : { [P in VectorFieldKey<E>]?: QueryVectorQuery };\n\n/**\n * Ordering by relevance to the `$text` at the root of `$where`, in either direction as any key sorts. The\n * object form also answers it under the name `$project` gives it, most relevant first unless `$order` says.\n */\nexport type QuerySortByText = {\n $text?: QuerySortDirection | { readonly $project: string; readonly $order?: QuerySortDirection };\n};\n\n/**\n * A row with the value a `$sort` projects under the name its `$project` gives - a vector's distance, or a\n * `$text` relevance - which is not inferred: `(await querier.findMany(Post, q)) as WithProjection<Post, 'score'>[]`.\n */\nexport type WithProjection<E, K extends string> = E & Record<K, number>;\n\n/**\n * A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count` or nearest row, a vector\n * distance, or - where `Root` says it sorts the queried entity itself, not a relation's rows - a `$text`\n * relevance or a distance it projects. One mapped type over the key sets: an intersection is checked once\n * per member, which made this the costliest.\n */\nexport type QuerySortMap<E, Root 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 size or nearest row.\n IsMany<E[P]> extends true\n ? QuerySortByCount | QuerySortByNearest<RelationTarget<E[P]>>\n : QuerySortMap<RelationTarget<E[P]>, false>\n : NonNullable<E[P]> extends readonly number[]\n ? Root extends true\n ? QuerySortValue\n : QuerySortDirection | QueryVectorQuery\n : QuerySortDirection;\n} & ([JsonFieldPaths<E>] extends [never] ? unknown : { [P in JsonFieldPaths<E>]?: QuerySortDirection }) &\n (Root 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": "F1CD14D3A7DD82FE64756E2164756E21",
16
+ "mappings": "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",
17
+ "debugId": "5397AE2C0FC782D564756E2164756E21",
18
18
  "names": []
19
19
  }
@@ -617,8 +617,9 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
617
617
  private appendRelationSubquery;
618
618
  /**
619
619
  * What a relation subquery selects: the literals a relation operator reads, or an aggregate over one
620
- * of the target's columns. `count` and `sum` answer `0` on a parent with no rows, which is what makes
621
- * them the two a trigger could keep; the rest answer `NULL`, and the field's type says so.
620
+ * of the target's columns or its distance to a vector. `count` and `sum` answer `0` on a parent with no
621
+ * rows, which is what makes them the two a trigger could keep; the rest answer `NULL`, and the field's
622
+ * type says so.
622
623
  */
623
624
  private aggregateProjection;
624
625
  /**
@@ -1,12 +1,12 @@
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, fieldUpdateOf, fulltextIndexOver, fulltextWeights, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, rankedTextSearch, targetKeyColumns, textSearchFields, textSortOf, textWeightSteps, 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, fulltextIndexOver, fulltextWeights, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, rankedTextSearch, targetKeyColumns, textSearchFields, textSortOf, textWeightSteps, parseGroupMap, parseRelationAtKey, parseRelationSize, 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';
8
8
  import { SqlQueryContext } from './queryContext.js';
9
- import { groupPathField, NO_JOINS, aggregateColumnField, resolveGroupJoins, resolveQueryJoins, resolveSortableJoin, } from './queryJoins.js';
9
+ import { groupPathField, NO_JOINS, aggregateColumnField, resolveGroupJoins, resolveQueryJoins, relationSortTerms, resolveSortableJoin, } from './queryJoins.js';
10
10
  import { resolveVectorCast } from './vectorCast.js';
11
11
  import { VectorSqlDialect } from './vectorSqlDialect.js';
12
12
  /** A scalar field's operator as the SQL arithmetic computing it. */
@@ -305,7 +305,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
305
305
  selectSortProjections(ctx, meta, q, prefix) {
306
306
  const distances = Object.entries(q.$sort ?? {}).flatMap(([key, value]) => isVectorSearch(value) && value.$project
307
307
  ? [
308
- this.sortProjection(ctx, meta, value.$project, (fragmentCtx) => this.appendVectorDistance(fragmentCtx, meta, key, value)),
308
+ this.sortProjection(ctx, meta, value.$project, (fragmentCtx) => this.appendVectorDistance(fragmentCtx, meta, key, value, prefix)),
309
309
  ]
310
310
  : []);
311
311
  const score = textSortOf(q.$sort)?.project;
@@ -598,7 +598,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
598
598
  return `NOT (${this.buildFragment(ctx, (fragmentCtx) => this.compare(fragmentCtx, entity, key, val, opts))})`;
599
599
  }
600
600
  if (op === '$near') {
601
- return this.vectorNearCondition(ctx, getMeta(entity), key, val);
601
+ return this.vectorNearCondition(ctx, getMeta(entity), key, val, opts.prefix);
602
602
  }
603
603
  const field = this.resolveOperandField(ctx, entity, key, opts);
604
604
  const condition = this.operatorCondition(ctx, field, op, val) ?? this.jsonArrayCondition(ctx, { base: field, path: '' }, op, val);
@@ -841,22 +841,26 @@ export class AbstractSqlDialect extends VectorSqlDialect {
841
841
  continue;
842
842
  }
843
843
  if (relation) {
844
- const countDirection = parseSortByCount(value);
845
- if (countDirection !== undefined) {
846
- // A correlated count, not a join: a parent has many of these, so what is being ordered by
847
- // is how many, and `SELECT DISTINCT` cannot order by an expression it did not select.
844
+ const { aggregates, rest } = relationSortTerms(key, keyPath, value);
845
+ for (const { spec, direction } of aggregates) {
846
+ // A correlated subquery, not a join: a parent has many of these, so what is being ordered by is
847
+ // one value over them, and `SELECT DISTINCT` cannot order by an expression it did not select.
848
+ const name = `${keyPath}.${spec.field ?? '$count'}`;
848
849
  if (opts.distinct) {
849
- throw new TypeError(`cannot $sort by '${keyPath}.$count' with $distinct: it is not a selected column`);
850
+ throw new TypeError(`cannot $sort by '${name}' with $distinct: it is not a selected column`);
850
851
  }
851
- columns.push({
852
- key: keyPath,
853
- expr: this.buildFragment(ctx, (fragmentCtx) => this.appendRelationSubquery(fragmentCtx, meta, key, relation, { prefix }, { op: '$count' })),
854
- direction: this.resolveSortDirection(countDirection),
855
- output: false,
856
- });
852
+ const expr = this.buildFragment(ctx, (fragmentCtx) => this.appendRelationAggregate(fragmentCtx, meta.entity, spec, prefix ?? ''));
853
+ if (spec.search) {
854
+ vectors.push({ key: name, expr, direction: '', output: false });
855
+ }
856
+ else {
857
+ columns.push({ key: keyPath, expr, direction: this.resolveSortDirection(direction), output: false });
858
+ }
859
+ }
860
+ if (rest === undefined) {
857
861
  continue;
858
862
  }
859
- const { join, sort: relationSort } = resolveSortableJoin(relation, keyPath, value, opts.joins ?? NO_JOINS, `cannot $sort by relation '${keyPath}': this statement joins no relations`);
863
+ const { join, sort: relationSort } = resolveSortableJoin(relation, keyPath, rest, opts.joins ?? NO_JOINS, `cannot $sort by relation '${keyPath}': this statement joins no relations`);
860
864
  // `SELECT DISTINCT` can only order by what it selected, on every engine here, so a join
861
865
  // brought in for the sort alone has nothing to order by. Populating it selects its columns.
862
866
  if (opts.distinct && !join.projected) {
@@ -866,15 +870,12 @@ export class AbstractSqlDialect extends VectorSqlDialect {
866
870
  continue;
867
871
  }
868
872
  if (isVectorSearch(value)) {
869
- if (path) {
870
- throw new TypeError(`$vector sort is only supported on the queried entity, not on relation '${path}'`);
871
- }
872
873
  // Already projected in the SELECT list: order by that alias rather than recomputing it.
873
874
  vectors.push(value.$project
874
875
  ? { key: keyPath, expr: this.escapeId(value.$project), direction: '', output: true }
875
876
  : {
876
877
  key: keyPath,
877
- expr: this.buildFragment(ctx, (fragmentCtx) => this.appendVectorDistance(fragmentCtx, meta, key, value)),
878
+ expr: this.buildFragment(ctx, (fragmentCtx) => this.appendVectorDistance(fragmentCtx, meta, key, value, prefix)),
878
879
  direction: '',
879
880
  output: false,
880
881
  });
@@ -1639,11 +1640,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1639
1640
  const parent = opts.prefix ?? this.resolveTableAlias(meta);
1640
1641
  // Resolved before any SQL is emitted: it also decides whether the junction form reaches the target.
1641
1642
  const targetWhere = this.scopedWhere(relatedMeta, read.where ?? {});
1642
- if (rel.through) {
1643
- // The rows here are the junction's own, so a column of the far side is read as a page instead.
1644
- if (read.field) {
1645
- throw new TypeError(`cannot read ${read.op}('${read.field}') over the many-to-many '${relKey}' without a page: its rows are the junction's, so name a '$sort' and a '$limit' to read the target's own`);
1646
- }
1643
+ // A tally or an existence reads the junction's own rows; a column of the target reads each target once.
1644
+ if (rel.through && !read.field) {
1647
1645
  ctx.append(`(SELECT ${read.op === 'exists' ? '1' : 'COUNT(*)'} FROM `);
1648
1646
  const junction = this.junctionRows(ctx, meta, rel, rel.through(), parent);
1649
1647
  ctx.append(junction.from);
@@ -1659,23 +1657,30 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1659
1657
  else {
1660
1658
  // The alias is claimed before the SELECT is written, since an aggregate names a column of it.
1661
1659
  const related = this.tableRef(relatedMeta, ctx.claimAlias(relKey, parent));
1662
- ctx.append(`(SELECT ${this.aggregateProjection(read, related.alias, relatedMeta)} FROM `);
1663
- ctx.append(related.ref);
1664
- ctx.append(` WHERE ${this.correlation(meta, rel, parent, related.alias, relatedMeta)}`);
1660
+ ctx.append(`(SELECT ${this.aggregateProjection(ctx, read, related.alias, relatedMeta)} FROM ${related.ref} WHERE `);
1661
+ this.appendCorrelation(ctx, meta, rel, parent, related.alias);
1665
1662
  this.renderWhere(ctx, relatedEntity, targetWhere, { prefix: related.alias, clause: 'AND' });
1666
1663
  }
1667
1664
  ctx.append(')');
1668
1665
  }
1669
1666
  /**
1670
1667
  * What a relation subquery selects: the literals a relation operator reads, or an aggregate over one
1671
- * of the target's columns. `count` and `sum` answer `0` on a parent with no rows, which is what makes
1672
- * them the two a trigger could keep; the rest answer `NULL`, and the field's type says so.
1668
+ * of the target's columns or its distance to a vector. `count` and `sum` answer `0` on a parent with no
1669
+ * rows, which is what makes them the two a trigger could keep; the rest answer `NULL`, and the field's
1670
+ * type says so.
1673
1671
  */
1674
- aggregateProjection(projection, alias, relatedMeta) {
1672
+ aggregateProjection(ctx, projection, alias, relatedMeta) {
1675
1673
  if (projection.op === 'exists') {
1676
1674
  return '1';
1677
1675
  }
1678
- return this.aggregateCall(projection.op, projection.field ? this.escapedColumn(alias, relatedMeta, projection.field) : '');
1676
+ const { op, field, search } = projection;
1677
+ if (!field) {
1678
+ return this.aggregateCall(op, '');
1679
+ }
1680
+ const operand = search
1681
+ ? this.buildFragment(ctx, (fragmentCtx) => this.appendVectorDistance(fragmentCtx, relatedMeta, field, search, alias))
1682
+ : this.escapedColumn(alias, relatedMeta, field);
1683
+ return this.aggregateCall(op, operand);
1679
1684
  }
1680
1685
  /**
1681
1686
  * A relation aggregate as the correlated subquery a `computed` field reads, the same one `$count`
@@ -1708,7 +1713,12 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1708
1713
  const correlation = raw(({ ctx: pageCtx }) => this.appendCorrelation(pageCtx, meta, rel, parent, alias));
1709
1714
  const { where: $where, page } = aggregate;
1710
1715
  // `1` where nothing is aggregated: a tally counts the rows the page holds, whatever they carry.
1711
- const read = aggregate.field ? refs(entity)[aggregate.field] : raw `1`;
1716
+ const { field, search } = aggregate;
1717
+ const read = !field
1718
+ ? raw `1`
1719
+ : search
1720
+ ? raw(({ ctx: readCtx }) => this.appendVectorDistance(readCtx, getMeta(entity), field, search, alias))
1721
+ : refs(entity)[field];
1712
1722
  const query = {
1713
1723
  ...page,
1714
1724
  $select: [read.as(AGGREGATE_VALUE_ALIAS)],
@@ -1882,7 +1892,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1882
1892
  }, 'unsupported $size comparison operator');
1883
1893
  }
1884
1894
  /** `<distance> <op> ?`, the `$where` half of a vector search, its bounds checked here since `/http` input is untyped. */
1885
- vectorNearCondition(ctx, meta, key, near) {
1895
+ vectorNearCondition(ctx, meta, key, near, prefix) {
1886
1896
  const bounds = {};
1887
1897
  for (const [op, val] of Object.entries(near)) {
1888
1898
  if (AbstractSqlDialect.VECTOR_QUERY_KEYS.has(op) || val === undefined) {
@@ -1900,12 +1910,10 @@ export class AbstractSqlDialect extends VectorSqlDialect {
1900
1910
  // Required by the type, so this only fires for a query that never met it: `/http` casts client
1901
1911
  // JSON straight to `Query`. A `$near` never borrows the `$sort`'s vector, which is what keeps the
1902
1912
  // predicate meaning the same thing in a `count`, or in an entity filter merged into a `$where`.
1903
- const { $vector, $distance } = near;
1904
- if (!$vector) {
1913
+ if (!near.$vector) {
1905
1914
  throw TypeError(`$near on '${key}' needs its own $vector`);
1906
1915
  }
1907
- const search = { $vector, $distance };
1908
- const distance = (fragmentCtx) => this.appendVectorDistance(fragmentCtx, meta, key, search);
1916
+ const distance = (fragmentCtx) => this.appendVectorDistance(fragmentCtx, meta, key, near, prefix);
1909
1917
  return this.boundConditions(ctx, distance, bounds, (operand, op, val) => this.operatorCondition(ctx, operand, op, val), 'unsupported $near bound');
1910
1918
  }
1911
1919
  /** ANSI-style single-quote escaping. MySQL-family dialects override this for backslash escaping. */
@@ -1,3 +1,4 @@
1
+ import type { RelationAggregateSpec } from '../type/index.js';
1
2
  /** The column a paged read carries its own unpaged total in, from `COUNT(*) OVER ()`. */
2
3
  export declare const TOTAL_ALIAS = "_uql_total";
3
4
  /**
@@ -36,11 +37,11 @@ export declare const UPSERT_NEW_ROW_ALIAS = "_uql_new";
36
37
  /** The row source a `MERGE` upsert reads its incoming values from, on SQL Server and Oracle. */
37
38
  export declare const UPSERT_SOURCE_ALIAS = "_uql_src";
38
39
  /**
39
- * Where a `$sort` by a relation's size parks its tally until the ordering has run. A function, so the
40
- * `$sort` that names the field and the stage that produces it cannot spell it differently - MongoDB
41
- * ranks a field that is not there as all-equal rather than failing, so a drift would go unnoticed.
40
+ * Where a `$sort` by a relation's aggregate - its size, or its nearest row - parks the value until the
41
+ * ordering has run. A function, so the `$sort` that names the field and the stage that produces it cannot
42
+ * spell it differently - MongoDB ranks a field that is not there as all-equal, so a drift would go unnoticed.
42
43
  */
43
- export declare function sortCountField(relKey: string): string;
44
+ export declare function sortAggregateField({ op, relation, field }: RelationAggregateSpec): string;
44
45
  /**
45
46
  * The column a relation's rows carry one sort term out in, beside the columns they answer under, for
46
47
  * the aggregate reading them to order by: `_uql_sort_createdAt`.
@@ -38,12 +38,12 @@ export const UPSERT_NEW_ROW_ALIAS = '_uql_new';
38
38
  /** The row source a `MERGE` upsert reads its incoming values from, on SQL Server and Oracle. */
39
39
  export const UPSERT_SOURCE_ALIAS = '_uql_src';
40
40
  /**
41
- * Where a `$sort` by a relation's size parks its tally until the ordering has run. A function, so the
42
- * `$sort` that names the field and the stage that produces it cannot spell it differently - MongoDB
43
- * ranks a field that is not there as all-equal rather than failing, so a drift would go unnoticed.
41
+ * Where a `$sort` by a relation's aggregate - its size, or its nearest row - parks the value until the
42
+ * ordering has run. A function, so the `$sort` that names the field and the stage that produces it cannot
43
+ * spell it differently - MongoDB ranks a field that is not there as all-equal, so a drift would go unnoticed.
44
44
  */
45
- export function sortCountField(relKey) {
46
- return `_uql_sort_count_${relKey}`;
45
+ export function sortAggregateField({ op, relation, field }) {
46
+ return `_uql_sort_${op.slice(1)}_${relation}${field ? `_${field}` : ''}`;
47
47
  }
48
48
  /**
49
49
  * The column a relation's rows carry one sort term out in, beside the columns they answer under, for
@@ -1,4 +1,4 @@
1
- import type { EntityMeta, FieldMeta, Query, QueryGroupMap, QuerySortMap, QueryWhere, RelationMeta, RelationQuery, Type } from '../type/index.js';
1
+ import type { EntityMeta, FieldMeta, Query, QueryGroupMap, QuerySortMap, QueryWhere, RelationAggregateSpec, RelationMeta, RelationQuery, Type } from '../type/index.js';
2
2
  import { type ParsedGroupEntry } from '../util/index.js';
3
3
  /**
4
4
  * One relation a statement joins, keyed by the alias its columns are addressed by (`tax`,
@@ -79,3 +79,16 @@ export declare function resolveSortableJoin(relation: RelationMeta, path: string
79
79
  readonly join: QueryJoin;
80
80
  readonly sort: QuerySortMap<object>;
81
81
  };
82
+ /** One ordering a relation's rows answer as a single value: their `$count`, or their nearest to a vector. */
83
+ export type RelationSortAggregate = {
84
+ readonly spec: RelationAggregateSpec;
85
+ readonly direction: unknown;
86
+ };
87
+ /**
88
+ * A relation's `$sort` value as the aggregates over its rows it orders by - its `$count`, or per vector
89
+ * field the distance of its nearest row - and what is left for a join to order by, if anything.
90
+ */
91
+ export declare function relationSortTerms(relKey: string, path: string, value: unknown): {
92
+ readonly aggregates: readonly RelationSortAggregate[];
93
+ readonly rest: unknown;
94
+ };
@@ -1,5 +1,5 @@
1
1
  import { getMeta, relationOf } from '../entity/index.js';
2
- import { getKeys, getRelationRequestSummary, isRecord, isToManyRelation, parseRelationAtKey, parseRelationSize, } from '../util/index.js';
2
+ import { getKeys, getRelationRequestSummary, hasKeys, isRecord, isToManyRelation, isVectorSearch, parseRelationAtKey, parseRelationSize, parseSortByCount, } from '../util/index.js';
3
3
  export const NO_JOINS = new Map();
4
4
  /**
5
5
  * What the statement joins, from `$populate` and from a `$sort` by a to-one relation's field, so the
@@ -137,12 +137,13 @@ function addPathJoins(joins, claimAlias, meta, map, required, parent) {
137
137
  const value = map[key];
138
138
  // A to-many, or a value that is not a map of the relation's own fields, cannot be joined and is
139
139
  // reported where the statement names it - the one place that knows how to.
140
- if (!relation || isToManyRelation(relation) || !isSortMap(value)) {
140
+ const fields = isSortMap(value) ? joinedSortFields(value) : undefined;
141
+ if (!relation || isToManyRelation(relation) || !fields) {
141
142
  continue;
142
143
  }
143
144
  const join = addJoin(joins, claimAlias, parent, key, relation, {}, required, false);
144
145
  // `E` stated: inferred from the nested map, it lands on the nested relation's target.
145
- addPathJoins(joins, claimAlias, join.meta, value, required, join);
146
+ addPathJoins(joins, claimAlias, join.meta, fields, required, join);
146
147
  }
147
148
  }
148
149
  /** The join a sort may address at `path` with the relation's own sort map, or why it may not; `unjoinable` is the dialect's remedy. */
@@ -159,6 +160,34 @@ export function resolveSortableJoin(relation, path, value, joins, unjoinable) {
159
160
  }
160
161
  return { join, sort: value };
161
162
  }
163
+ /**
164
+ * A relation's `$sort` value as the aggregates over its rows it orders by - its `$count`, or per vector
165
+ * field the distance of its nearest row - and what is left for a join to order by, if anything.
166
+ */
167
+ export function relationSortTerms(relKey, path, value) {
168
+ const count = parseSortByCount(value);
169
+ if (count !== undefined) {
170
+ return { aggregates: [{ spec: { relation: relKey, op: '$count' }, direction: count }], rest: undefined };
171
+ }
172
+ if (!isSortMap(value)) {
173
+ return { aggregates: [], rest: value };
174
+ }
175
+ const aggregates = Object.entries(value).flatMap(([field, search]) => {
176
+ if (!isVectorSearch(search)) {
177
+ return [];
178
+ }
179
+ if (search.$project !== undefined) {
180
+ throw new TypeError(`cannot $project the distance of relation '${path}': it ranks the parent, and no one row answers under it`);
181
+ }
182
+ return [{ spec: { relation: relKey, op: '$min', field, search }, direction: undefined }];
183
+ });
184
+ return { aggregates, rest: joinedSortFields(value) };
185
+ }
186
+ /** The fields of a relation's sort map a join orders by: all but its vector searches, which rank its nearest row. */
187
+ function joinedSortFields(map) {
188
+ const fields = Object.fromEntries(Object.entries(map).filter(([, value]) => !isVectorSearch(value)));
189
+ return hasKeys(fields) ? fields : undefined;
190
+ }
162
191
  /** A nested map of fields, as opposed to a `$sort` direction or vector search, or a `$group` field's `true`. */
163
192
  function isSortMap(value) {
164
193
  return typeof value === 'object' && value !== null && !Array.isArray(value) && !('$vector' in value);
@@ -1,4 +1,4 @@
1
- import type { EntityIndexMeta, EntityMeta, FieldOptions, Query, QueryContext, QueryVectorSearch, QueryWhere, SqlDialectFeatures, VectorDistance, VectorMetric } from '../type/index.js';
1
+ import type { EntityIndexMeta, EntityMeta, FieldOptions, Query, QueryContext, QueryVectorQuery, QueryWhere, SqlDialectFeatures, VectorDistance, VectorMetric } from '../type/index.js';
2
2
  import { AbstractDialect } from './abstractDialect.js';
3
3
  import { type VectorCast } from './vectorCast.js';
4
4
  /**
@@ -32,11 +32,8 @@ export declare abstract class VectorSqlDialect extends AbstractDialect {
32
32
  hasVectorIndex(): boolean;
33
33
  /** Quotes an identifier; supplied by the SQL dialect built on top of this layer. */
34
34
  abstract escapeId(val: string | undefined, forbidQualified?: boolean, addDot?: boolean): string;
35
- /**
36
- * What a distance expression reads, for a `$sort` and a `$near` alike. The metric falls back to the
37
- * field's, then its index's, which serves no other, then cosine.
38
- */
39
- protected resolveVectorDistance<E>(meta: EntityMeta<E>, key: string, search: QueryVectorSearch): {
35
+ /** What a distance expression reads, for a `$sort` and a `$near` alike. */
36
+ protected resolveVectorDistance<E>(meta: EntityMeta<E>, key: string, search: QueryVectorQuery): {
40
37
  colName: string;
41
38
  distance: VectorDistance;
42
39
  field: FieldOptions | undefined;
@@ -53,7 +50,8 @@ export declare abstract class VectorSqlDialect extends AbstractDialect {
53
50
  supportedVectorType(cast: VectorCast): VectorCast;
54
51
  /**
55
52
  * The distance expression, in whichever of the two shapes this dialect spells it. One method for
56
- * both, so the metric lookup and its refusal exist once rather than per shape.
53
+ * both, so the metric lookup and its refusal exist once rather than per shape. The column is read
54
+ * under `prefix`, the alias in scope, since a joined table may have a column of the same name.
57
55
  */
58
- protected appendVectorDistance<E>(ctx: QueryContext, meta: EntityMeta<E>, key: string, search: QueryVectorSearch): void;
56
+ protected appendVectorDistance<E>(ctx: QueryContext, meta: EntityMeta<E>, key: string, search: QueryVectorQuery, prefix: string | undefined): void;
59
57
  }
@@ -1,5 +1,5 @@
1
- import { DEFAULT_VECTOR_DISTANCE, unsupportedVectorMetric } from '../type/vector.js';
2
- import { findVectorIndex, findVectorSort, vectorCandidates } from '../util/dialect.util.js';
1
+ import { unsupportedVectorMetric } from '../type/vector.js';
2
+ import { findVectorIndex, findVectorSort, vectorCandidates, vectorDistanceOf } from '../util/dialect.util.js';
3
3
  import { AbstractDialect } from './abstractDialect.js';
4
4
  import { encodeFloat32s } from './vectorCast.js';
5
5
  /**
@@ -44,15 +44,10 @@ export class VectorSqlDialect extends AbstractDialect {
44
44
  hasVectorIndex() {
45
45
  return [...this.vectorMetrics.values()].some((metric) => metric.index);
46
46
  }
47
- /**
48
- * What a distance expression reads, for a `$sort` and a `$near` alike. The metric falls back to the
49
- * field's, then its index's, which serves no other, then cosine.
50
- */
47
+ /** What a distance expression reads, for a `$sort` and a `$near` alike. */
51
48
  resolveVectorDistance(meta, key, search) {
52
49
  const field = meta.fields[key];
53
- const colName = this.resolveColumnName(key, field);
54
- const distance = search.$distance ?? field?.distance ?? findVectorIndex(meta, key)?.distance ?? DEFAULT_VECTOR_DISTANCE;
55
- return { colName, distance, field };
50
+ return { colName: this.resolveColumnName(key, field), distance: vectorDistanceOf(meta, key, search), field };
56
51
  }
57
52
  /**
58
53
  * Binds a vector, both as a persisted value and as the query vector of a distance expression, so a
@@ -70,9 +65,10 @@ export class VectorSqlDialect extends AbstractDialect {
70
65
  }
71
66
  /**
72
67
  * The distance expression, in whichever of the two shapes this dialect spells it. One method for
73
- * both, so the metric lookup and its refusal exist once rather than per shape.
68
+ * both, so the metric lookup and its refusal exist once rather than per shape. The column is read
69
+ * under `prefix`, the alias in scope, since a joined table may have a column of the same name.
74
70
  */
75
- appendVectorDistance(ctx, meta, key, search) {
71
+ appendVectorDistance(ctx, meta, key, search, prefix) {
76
72
  if (this.vectorMetrics.size === 0) {
77
73
  throw new TypeError(`${this.dialectName} does not support vector similarity search. Use raw() for vector queries.`);
78
74
  }
@@ -81,14 +77,15 @@ export class VectorSqlDialect extends AbstractDialect {
81
77
  if (!metric) {
82
78
  throw unsupportedVectorMetric(this.dialectName, distance);
83
79
  }
80
+ const column = this.escapeId(prefix, true, true) + this.escapeId(colName);
84
81
  if ('fn' in metric) {
85
82
  const leading = metric.metricArg === undefined ? '' : `'${metric.metricArg}', `;
86
- ctx.append(`${metric.fn}(${leading}${this.escapeId(colName)}, `);
83
+ ctx.append(`${metric.fn}(${leading}${column}, `);
87
84
  this.appendVectorValue(ctx, search.$vector, field);
88
85
  ctx.append(')');
89
86
  return;
90
87
  }
91
- ctx.append(`${this.escapeId(colName)} ${metric.op} `);
88
+ ctx.append(`${column} ${metric.op} `);
92
89
  this.appendVectorValue(ctx, search.$vector, field);
93
90
  }
94
91
  }
@@ -130,11 +130,11 @@ export declare class MongoDialect extends AbstractDialect {
130
130
  /** Walks `$sort` against the metadata of the entity each level addresses, as the SQL dialects do. */
131
131
  private collectSort;
132
132
  /**
133
- * The stages a `$sort` by a relation's size needs: one correlated `$lookup` tallying the relation
134
- * per parent, and the `$set` that lifts the tally onto the document as the field the `$sort` then
135
- * orders by. A parent with no related row gets no lookup result at all, which is a zero.
133
+ * The stages a `$sort` by a relation's aggregate needs - its size, or its row nearest a vector: one
134
+ * correlated `$lookup` reading it per parent, and the `$set` that lifts it onto the document as the
135
+ * field the `$sort` then orders by. A parent with no related row reads a zero tally and no distance.
136
136
  */
137
- sortCountStages<E extends Document>(entity: Type<E>, sort: QuerySortMap<E> | undefined): {
137
+ sortAggregateStages<E extends Document>(entity: Type<E>, sort: QuerySortMap<E> | undefined): {
138
138
  readonly stages: MongoAggregationPipelineEntry<Document>[];
139
139
  readonly fields: string[];
140
140
  };
@@ -1,13 +1,14 @@
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, TEXT_SCORE_ALIAS, } from '../dialect/aliases.js';
4
- import { aggregateColumnField, groupPathField, resolveGroupJoins, resolveQueryJoins, resolveSortableJoin, } from '../dialect/queryJoins.js';
3
+ import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, sortAggregateField, TEXT_SCORE_ALIAS, } from '../dialect/aliases.js';
4
+ import { aggregateColumnField, groupPathField, resolveGroupJoins, relationSortTerms, 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, fieldUpdateOf, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, rankedTextSearch, someKey, targetKeyColumns, textSortOf, } 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, rankedTextSearch, someKey, targetKeyColumns, textSortOf, vectorDistanceOf, } from '../util/index.js';
9
9
  import { decodeBigIntsExcept } from '../util/wideNumber.js';
10
10
  import { textLanguage } from './textLanguage.js';
11
+ import { vectorDistanceExpr } from './vectorDistance.js';
11
12
  /** A scalar field's operator as the aggregation operator computing it. */
12
13
  const MONGO_ARITHMETIC = { $inc: '$add', $mul: '$multiply' };
13
14
  /** Default {@link DialectFeatures} for MongoDB. */
@@ -461,7 +462,8 @@ export class MongoDialect extends AbstractDialect {
461
462
  */
462
463
  sort(entity, { $sort: sort, $populate: populate, $where: where }) {
463
464
  const meta = getMeta(entity);
464
- const normalized = {};
465
+ const nearest = {};
466
+ const columns = {};
465
467
  // Refused as the SQL dialects refuse it, before MongoDB answers a missing score with its own error.
466
468
  if (sort?.$text) {
467
469
  rankedTextSearch(where);
@@ -469,11 +471,13 @@ export class MongoDialect extends AbstractDialect {
469
471
  // The same join set the lookups are built from, so what an ordering may address and what the
470
472
  // pipeline actually produces cannot drift apart - `$sort` contributes its own to-one joins here
471
473
  // exactly as it does on the SQL dialects.
472
- this.collectSort(meta, sort, resolveQueryJoins(meta, { $populate: populate, $sort: sort }), '', normalized);
473
- return normalized;
474
+ const joins = resolveQueryJoins(meta, { $populate: populate, $sort: sort });
475
+ this.collectSort(meta, sort, joins, '', nearest, columns);
476
+ // A vector distance is the primary ordering wherever it appears, as on the SQL dialects.
477
+ return { ...nearest, ...columns };
474
478
  }
475
479
  /** Walks `$sort` against the metadata of the entity each level addresses, as the SQL dialects do. */
476
- collectSort(meta, sort, joins, path, out) {
480
+ collectSort(meta, sort, joins, path, nearest, out) {
477
481
  for (const [key, value] of Object.entries(sort ?? {})) {
478
482
  const relation = meta.relations[key];
479
483
  if (key === '$text') {
@@ -485,12 +489,11 @@ export class MongoDialect extends AbstractDialect {
485
489
  continue;
486
490
  }
487
491
  if (!relation) {
488
- // The queried entity's own vector search is lifted out before this walk, so one reaching it
489
- // sits under a relation, which a `$lookup` brings in one row at a time - there is nothing to
490
- // rank. `sortDirection` would read the operator object as "ascending" and order by the raw
491
- // vector column instead, which is the SQL dialects' rejection turned into a silent answer.
492
+ // The queried entity's first vector search is lifted out into `$vectorSearch` before this walk,
493
+ // so one reaching it is a second. `sortDirection` would read the operator object as "ascending"
494
+ // and order by the raw vector column instead, a silent answer where the caller asked for a rank.
492
495
  if (isVectorSearch(value)) {
493
- throw new TypeError(`$vector sort is only supported on the queried entity, not on relation '${path.slice(0, -1)}'`);
496
+ throw new TypeError(`cannot $sort by a second vector '${key}' on MongoDB: $vectorSearch ranks by one`);
494
497
  }
495
498
  out[path + this.pathOf(meta, key)] = sortDirection(value);
496
499
  continue;
@@ -499,39 +502,38 @@ export class MongoDialect extends AbstractDialect {
499
502
  // one: ordering by a relation nothing looked up reads a field that is not there, which MongoDB
500
503
  // ranks as all-equal rather than rejecting. The SQL dialects can add the join themselves.
501
504
  const relPath = `${path}${key}`;
502
- const countDirection = parseSortByCount(value);
503
- if (countDirection !== undefined) {
504
- // The tally rides on a field {@link sortCountStages} adds, which only the queried entity's
505
+ const { aggregates, rest } = relationSortTerms(key, relPath, value);
506
+ for (const { spec, direction } of aggregates) {
507
+ // The value rides on a field {@link sortAggregateStages} adds, which only the queried entity's
505
508
  // own pipeline has: a nested one is built inside its parent's `$lookup`, where there is no
506
509
  // parent document left to hang it off.
507
510
  if (path) {
508
- throw new TypeError(`$sort by '${relPath}.$count' is only supported on the queried entity`);
511
+ throw new TypeError(`$sort by '${relPath}.${spec.field ?? '$count'}' is only supported on the queried entity`);
512
+ }
513
+ if (spec.search) {
514
+ nearest[sortAggregateField(spec)] = 1;
515
+ }
516
+ else {
517
+ out[sortAggregateField(spec)] = sortDirection(direction);
509
518
  }
510
- out[sortCountField(key)] = sortDirection(countDirection);
519
+ }
520
+ if (rest === undefined) {
511
521
  continue;
512
522
  }
513
- const { join, sort: relationSort } = resolveSortableJoin(relation, relPath, value, joins, `cannot $sort by relation '${relPath}' on MongoDB unless it is populated: only $populate adds its fields to the document`);
514
- this.collectSort(join.meta, relationSort, joins, `${relPath}.`, out);
523
+ const { join, sort: relationSort } = resolveSortableJoin(relation, relPath, rest, joins, `cannot $sort by relation '${relPath}' on MongoDB unless it is populated: only $populate adds its fields to the document`);
524
+ this.collectSort(join.meta, relationSort, joins, `${relPath}.`, nearest, out);
515
525
  }
516
526
  }
517
527
  /**
518
- * The stages a `$sort` by a relation's size needs: one correlated `$lookup` tallying the relation
519
- * per parent, and the `$set` that lifts the tally onto the document as the field the `$sort` then
520
- * orders by. A parent with no related row gets no lookup result at all, which is a zero.
528
+ * The stages a `$sort` by a relation's aggregate needs - its size, or its row nearest a vector: one
529
+ * correlated `$lookup` reading it per parent, and the `$set` that lifts it onto the document as the
530
+ * field the `$sort` then orders by. A parent with no related row reads a zero tally and no distance.
521
531
  */
522
- sortCountStages(entity, sort) {
532
+ sortAggregateStages(entity, sort) {
523
533
  const meta = getMeta(entity);
524
- const stages = [];
525
- const fields = [];
526
- for (const [key, value] of Object.entries(sort ?? {})) {
527
- const relOpts = meta.relations[key];
528
- if (!relOpts || parseSortByCount(value) === undefined) {
529
- continue;
530
- }
531
- const temp = sortCountField(key);
532
- stages.push(...this.aggregateStages(meta, { relation: key, op: '$count' }, `${REL_TEMP_PREFIX}${temp}`, temp));
533
- fields.push(temp);
534
- }
534
+ const specs = Object.entries(sort ?? {}).flatMap(([key, value]) => meta.relations[key] ? relationSortTerms(key, key, value).aggregates.map(({ spec }) => spec) : []);
535
+ const fields = specs.map(sortAggregateField);
536
+ const stages = specs.flatMap((spec, index) => this.aggregateStages(meta, spec, `${REL_TEMP_PREFIX}${fields[index]}`, fields[index]));
535
537
  return { stages, fields };
536
538
  }
537
539
  /** Whether a read answers with a relation aggregate, which only the pipeline can build. */
@@ -568,15 +570,24 @@ export class MongoDialect extends AbstractDialect {
568
570
  */
569
571
  aggregateStages(meta, spec, temp, field) {
570
572
  const relOpts = relationOf(meta, spec.relation);
573
+ const relMeta = getMeta(relOpts.entity());
571
574
  const page = spec.page ?? {};
575
+ // A many-to-many's lookup runs over its junction's rows, each carrying its target: read as that
576
+ // target, so the page's order and the aggregate reach the target's own fields.
577
+ const targets = relOpts.through ? [{ $replaceRoot: { newRoot: { $arrayElemAt: [`$${REL_NESTED_KEY}`, 0] } } }] : [];
572
578
  const tail = [
579
+ ...targets,
573
580
  ...(page.$sort ? [{ $sort: this.sort(relOpts.entity(), page) }] : []),
574
581
  ...this.pagerStages(page),
575
582
  spec.field
576
583
  ? {
577
584
  $group: {
578
585
  _id: null,
579
- [AGGREGATE_VALUE_ALIAS]: { [spec.op]: `$${this.columnOf(getMeta(relOpts.entity()), spec.field)}` },
586
+ [AGGREGATE_VALUE_ALIAS]: {
587
+ [spec.op]: spec.search
588
+ ? vectorDistanceExpr(this.columnOf(relMeta, spec.field), spec.search.$vector, vectorDistanceOf(relMeta, spec.field, spec.search))
589
+ : `$${this.columnOf(relMeta, spec.field)}`,
590
+ },
580
591
  },
581
592
  }
582
593
  : { $count: AGGREGATE_VALUE_ALIAS },
@@ -708,10 +719,10 @@ export class MongoDialect extends AbstractDialect {
708
719
  readStages(entity, q, extra = {}) {
709
720
  const meta = getMeta(entity);
710
721
  const joins = resolveQueryJoins(meta, q);
711
- // The tally an ordering by a relation's size reads, and the field it parks it on: both belong
722
+ // The value an ordering by a relation's aggregate reads, and the field it parks it on: both belong
712
723
  // with the lookups, since the `$sort` right after them is what they exist for.
713
- const counted = this.sortCountStages(entity, q.$sort);
714
- const lookups = [...this.lookupStages(meta, joins), ...counted.stages];
724
+ const aggregated = this.sortAggregateStages(entity, q.$sort);
725
+ const lookups = [...this.lookupStages(meta, joins), ...aggregated.stages];
715
726
  // Each to-many and each `$count`, which neither drop nor reorder a row, so they read the page alone.
716
727
  const related = this.relationReadStages(entity, q);
717
728
  const sort = hasKeys(extra.sort) ? [{ $sort: extra.sort }] : [];
@@ -732,13 +743,13 @@ export class MongoDialect extends AbstractDialect {
732
743
  // which is the one way this differs from a SQL join. Taken back out once the `$sort` that needed
733
744
  // it has run, so ordering by an unpopulated relation costs the same nothing it does there.
734
745
  const sortOnly = [...joins.values()].filter((join) => !join.projected).map((join) => join.path);
735
- const dropped = [...sortOnly, ...counted.fields];
746
+ const dropped = [...sortOnly, ...aggregated.fields];
736
747
  const unset = dropped.length ? [{ $unset: dropped }] : [];
737
748
  // The grouping collapses rows onto the columns it projects, which leaves nothing for an ordering
738
749
  // that reads a lookup those columns do not carry. Refused rather than answered all-equal, and in
739
750
  // the same terms the SQL dialects refuse `SELECT DISTINCT` ordered by an unselected column.
740
- if (q.$distinct && counted.fields.length) {
741
- throw new TypeError(`cannot $sort by a relation's $count with $distinct: the grouping keeps only the columns it projects`);
751
+ if (q.$distinct && aggregated.fields.length) {
752
+ throw new TypeError(`cannot $sort by a relation's aggregate with $distinct: the grouping keeps only the columns it projects`);
742
753
  }
743
754
  if (q.$distinct && sortOnly.length) {
744
755
  throw new TypeError(`cannot $sort by relation '${sortOnly[0]}' with $distinct unless '${sortOnly[0]}' is populated: the grouping keeps only the columns it projects`);
@@ -0,0 +1,8 @@
1
+ import type { Document } from 'mongodb';
2
+ import type { VectorDistance } from '../type/index.js';
3
+ /**
4
+ * A document's distance from `vector` as an aggregation expression, computed exactly as the SQL engines
5
+ * compute theirs: Atlas ranks only through its own index, which no related document reaches. `null` where
6
+ * the field holds no vector, or a cosine has a zero-length side, and `$min` skips a `null`.
7
+ */
8
+ export declare function vectorDistanceExpr(column: string, vector: readonly number[], metric: VectorDistance): Document;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A document's distance from `vector` as an aggregation expression, computed exactly as the SQL engines
3
+ * compute theirs: Atlas ranks only through its own index, which no related document reaches. `null` where
4
+ * the field holds no vector, or a cosine has a zero-length side, and `$min` skips a `null`.
5
+ */
6
+ export function vectorDistanceExpr(column, vector, metric) {
7
+ const field = `$${column}`;
8
+ const sum = (term) => ({
9
+ $sum: { $map: { input: { $zip: { inputs: [field, { $literal: vector }] } }, as: 'pair', in: term } },
10
+ });
11
+ const own = { $arrayElemAt: ['$$pair', 0] };
12
+ const other = { $arrayElemAt: ['$$pair', 1] };
13
+ const dot = sum({ $multiply: [own, other] });
14
+ const distance = {
15
+ cosine: {
16
+ $let: {
17
+ vars: { norms: { $multiply: [{ $sqrt: sum({ $multiply: [own, own] }) }, Math.hypot(...vector)] } },
18
+ in: { $cond: [{ $eq: ['$$norms', 0] }, null, { $subtract: [1, { $divide: [dot, '$$norms'] }] }] },
19
+ },
20
+ },
21
+ l2: { $sqrt: sum({ $pow: [{ $subtract: [own, other] }, 2] }) },
22
+ inner: { $multiply: [-1, dot] },
23
+ l1: sum({ $abs: { $subtract: [own, other] } }),
24
+ };
25
+ return { $cond: [{ $isArray: field }, distance[metric], null] };
26
+ }
@@ -3,6 +3,7 @@ import type { Query, QueryConflictPaths, QueryOptions, QueryPage, QuerySearch, R
3
3
  import type { QueryAggMap, QueryAggregate, QueryAggregateOp, QueryGroupMap } from './queryAggregate.js';
4
4
  import type { QueryWhere } from './queryWhere.js';
5
5
  import type { Type } from './utility.js';
6
+ import type { QueryVectorQuery } from './vector.js';
6
7
  /**
7
8
  * comparison options.
8
9
  */
@@ -247,13 +248,15 @@ export type AggregateCall<E = object> = {
247
248
  readonly field?: string;
248
249
  readonly where?: QueryWhere<E>;
249
250
  };
250
- /** What a relation aggregate reads: how many rows, or one of the target's columns. */
251
+ /** What a relation aggregate reads: how many rows, or one of the target's columns, or its distance to `search`. */
251
252
  export type RelationAggregateProjection = {
252
253
  readonly op: '$count';
253
254
  readonly field?: never;
255
+ readonly search?: never;
254
256
  } | {
255
257
  readonly op: Exclude<RelationAggregateOp, '$count'>;
256
258
  readonly field: string;
259
+ readonly search?: QueryVectorQuery;
257
260
  };
258
261
  /**
259
262
  * A relation aggregate as a `computed` field holds it: an {@link AggregateCall} over the rows of the
@@ -3,7 +3,7 @@ import type { QueryLock } from './queryLock.js';
3
3
  import type { QueryRaw } from './queryRaw.js';
4
4
  import type { QueryWhere } from './queryWhere.js';
5
5
  import type { BooleanLike, Except, IsMany, PrimaryKey } from './utility.js';
6
- import type { QueryVectorSearch } from './vector.js';
6
+ import type { QueryVectorQuery, QueryVectorSearch } from './vector.js';
7
7
  export type QueryOptions = {
8
8
  /**
9
9
  * Toggle named entity filters for this query. `false` disables all filters;
@@ -129,6 +129,18 @@ export type QuerySortValue = QuerySortDirection | QueryVectorSearch;
129
129
  export type QuerySortByCount = {
130
130
  $count: QuerySortDirection;
131
131
  };
132
+ /** The fields of `E` a vector search can rank by. */
133
+ type VectorFieldKey<E> = {
134
+ [P in FieldKey<E>]: NonNullable<E[P]> extends readonly number[] ? P : never;
135
+ }[FieldKey<E>];
136
+ /**
137
+ * Ordering parents by the row of a to-many nearest a vector, per vector field: its distance is the
138
+ * smallest of theirs. Nothing to `$project`, since no one row of the parent's answers under it. Never
139
+ * where the target has no vector, since an empty map would admit any value at all.
140
+ */
141
+ export type QuerySortByNearest<E> = [VectorFieldKey<E>] extends [never] ? never : {
142
+ [P in VectorFieldKey<E>]?: QueryVectorQuery;
143
+ };
132
144
  /**
133
145
  * Ordering by relevance to the `$text` at the root of `$where`, in either direction as any key sorts. The
134
146
  * object form also answers it under the name `$project` gives it, most relevant first unless `$order` says.
@@ -145,12 +157,13 @@ export type QuerySortByText = {
145
157
  */
146
158
  export type WithProjection<E, K extends string> = E & Record<K, number>;
147
159
  /**
148
- * A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count`, or - where `Root` says it
149
- * sorts the queried entity itself, not a relation's rows - a vector distance or a `$text` relevance. One
150
- * mapped type over the key sets: an intersection is checked once per member, which made this the costliest.
160
+ * A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count` or nearest row, a vector
161
+ * distance, or - where `Root` says it sorts the queried entity itself, not a relation's rows - a `$text`
162
+ * relevance or a distance it projects. One mapped type over the key sets: an intersection is checked once
163
+ * per member, which made this the costliest.
151
164
  */
152
165
  export type QuerySortMap<E, Root extends boolean = true, K extends keyof E = FieldKey<E> | RelationKey<E>> = {
153
- [P in K]?: P extends RelationKey<E> ? IsMany<E[P]> extends true ? QuerySortByCount : QuerySortMap<RelationTarget<E[P]>, false> : Root extends true ? NonNullable<E[P]> extends readonly number[] ? QuerySortValue : QuerySortDirection : QuerySortDirection;
166
+ [P in K]?: P extends RelationKey<E> ? IsMany<E[P]> extends true ? QuerySortByCount | QuerySortByNearest<RelationTarget<E[P]>> : QuerySortMap<RelationTarget<E[P]>, false> : NonNullable<E[P]> extends readonly number[] ? Root extends true ? QuerySortValue : QuerySortDirection | QueryVectorQuery : QuerySortDirection;
154
167
  } & ([JsonFieldPaths<E>] extends [never] ? unknown : {
155
168
  [P in JsonFieldPaths<E>]?: QuerySortDirection;
156
169
  }) & (Root extends true ? QuerySortByText : unknown);
@@ -1,5 +1,5 @@
1
1
  import type { IndexType } from '../schema/types.js';
2
- import { type AggregateCall, 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 QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySortDirection, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorSearch, type QueryWhere, type RelationKey, type UpdatePayload } from '../type/index.js';
2
+ import { type AggregateCall, 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 QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySortDirection, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorQuery, type QueryVectorSearch, type QueryWhere, type RelationKey, type UpdatePayload, type VectorDistance } from '../type/index.js';
3
3
  export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
4
4
  /** The keys of `payload` a write persists as columns. */
5
5
  export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E> | UpdatePayload<E>, callbackKey: CallbackKey): FieldKey<E>[];
@@ -81,6 +81,11 @@ export declare function vectorCandidates(q: {
81
81
  * "which kind", which decide the name Atlas is queried by and the setting Postgres is tuned with.
82
82
  */
83
83
  export declare function findVectorIndex<E>(meta: EntityMeta<E>, key: string): EntityIndexMeta<E> | undefined;
84
+ /**
85
+ * The metric a distance to `key` measures by: the search's own, else the field's, else its index's, which
86
+ * serves no other, else cosine. The one fallback every engine resolves, so none can rank by another.
87
+ */
88
+ export declare function vectorDistanceOf<E>(meta: EntityMeta<E>, key: string, search: QueryVectorQuery): VectorDistance;
84
89
  /**
85
90
  * Whether a `$where` filters by vector distance anywhere in its tree, `$and`/`$or`/`$not` included.
86
91
  * What tells Postgres that an HNSW scan needs to iterate rather than return one candidate list.
@@ -1,7 +1,7 @@
1
1
  import { getContext, UqlSecurityError } from '../context/context.js';
2
2
  import { soleIdOf } from '../entity/metadata/definition.js';
3
3
  import { QueryRaw, resolveAggregateOp, SOFT_DELETE_FILTER, } from '../type/index.js';
4
- import { VECTOR_INDEX_TYPES } from '../type/vector.js';
4
+ import { DEFAULT_VECTOR_DISTANCE, VECTOR_INDEX_TYPES } from '../type/vector.js';
5
5
  import { getFieldKeys, isDatabaseWritten } from './field.util.js';
6
6
  import { entityName, getKeys, hasKeys, isOperatorObject, isScalarId, isRecord, isWhereMap, someKey, } from './object.util.js';
7
7
  /** The keys of `payload` a write persists as columns. */
@@ -209,6 +209,16 @@ const VECTOR_INDEX_MATCH = new Set([...VECTOR_INDEX_TYPES, 'vectorSearch']);
209
209
  export function findVectorIndex(meta, key) {
210
210
  return meta.indexes?.find((index) => index.type !== undefined && VECTOR_INDEX_MATCH.has(index.type) && indexCoversColumn(index, key));
211
211
  }
212
+ /**
213
+ * The metric a distance to `key` measures by: the search's own, else the field's, else its index's, which
214
+ * serves no other, else cosine. The one fallback every engine resolves, so none can rank by another.
215
+ */
216
+ export function vectorDistanceOf(meta, key, search) {
217
+ return (search.$distance ??
218
+ meta.fields[key]?.distance ??
219
+ findVectorIndex(meta, key)?.distance ??
220
+ DEFAULT_VECTOR_DISTANCE);
221
+ }
212
222
  /**
213
223
  * Whether a `$where` filters by vector distance anywhere in its tree, `$and`/`$or`/`$not` included.
214
224
  * What tells Postgres that an HNSW scan needs to iterate rather than return one candidate list.
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.73.1",
6
+ "version": "0.74.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"