@_linked/core 2.18.1 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +24 -3
- package/lib/esm/index.d.ts +3 -0
- package/lib/esm/index.js +1 -0
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/interfaces/IDataset.d.ts +23 -0
- package/lib/esm/queries/CountBuilder.d.ts +69 -0
- package/lib/esm/queries/CountBuilder.js +162 -0
- package/lib/esm/queries/CountBuilder.js.map +1 -0
- package/lib/esm/queries/CountQuery.d.ts +80 -0
- package/lib/esm/queries/CountQuery.js +7 -0
- package/lib/esm/queries/CountQuery.js.map +1 -0
- package/lib/esm/queries/IntermediateRepresentation.d.ts +31 -1
- package/lib/esm/queries/QueryBuilder.d.ts +80 -0
- package/lib/esm/queries/QueryBuilder.js +98 -13
- package/lib/esm/queries/QueryBuilder.js.map +1 -1
- package/lib/esm/queries/fromJSON.d.ts +5 -3
- package/lib/esm/queries/fromJSON.js +4 -1
- package/lib/esm/queries/fromJSON.js.map +1 -1
- package/lib/esm/queries/lower.d.ts +17 -2
- package/lib/esm/queries/lower.js +61 -0
- package/lib/esm/queries/lower.js.map +1 -1
- package/lib/esm/queries/queryDispatch.d.ts +35 -0
- package/lib/esm/queries/queryDispatch.js +36 -0
- package/lib/esm/queries/queryDispatch.js.map +1 -1
- package/lib/esm/shapes/Shape.d.ts +32 -0
- package/lib/esm/shapes/Shape.js +47 -0
- package/lib/esm/shapes/Shape.js.map +1 -1
- package/lib/esm/sparql/SparqlDataset.d.ts +11 -0
- package/lib/esm/sparql/SparqlDataset.js +19 -2
- package/lib/esm/sparql/SparqlDataset.js.map +1 -1
- package/lib/esm/sparql/index.d.ts +3 -3
- package/lib/esm/sparql/index.js +3 -3
- package/lib/esm/sparql/index.js.map +1 -1
- package/lib/esm/sparql/irToAlgebra.d.ts +32 -1
- package/lib/esm/sparql/irToAlgebra.js +74 -0
- package/lib/esm/sparql/irToAlgebra.js.map +1 -1
- package/lib/esm/sparql/resultMapping.d.ts +14 -1
- package/lib/esm/sparql/resultMapping.js +40 -0
- package/lib/esm/sparql/resultMapping.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.19.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#224](https://github.com/linked-cm/core/pull/224) [`ea9971c`](https://github.com/linked-cm/core/commit/ea9971c6201f610102ad7674868602e1aaeb4e1f) Thanks [@flyon](https://github.com/flyon)! - Adds a root-level count to the query DSL: `SelectBuilder.from(shape).where(…).count()` and
|
|
8
|
+
`Shape.count()` resolve to a real `number`, lowering to `SELECT (COUNT(DISTINCT ?s) AS ?count)
|
|
9
|
+
WHERE { … }`. Like an ask, a count is its own builder and IR kind, so `limit`/`offset` are dropped at
|
|
10
|
+
the boundary and unrepresentable thereafter — the count of a window is the count of the whole match
|
|
11
|
+
set. `.toCount()` is public so a router can forward the `{op: 'count'}` envelope instead of executing
|
|
12
|
+
it.
|
|
13
|
+
|
|
14
|
+
`IDataset.countQuery` is **optional**, so nothing breaks: every store extending `SparqlDataset` gets
|
|
15
|
+
it with no edit. A store or router that does not extend `SparqlDataset` — including any
|
|
16
|
+
`setQueryDispatch({…})` object literal in a consuming package — needs a `countQuery` arm added by
|
|
17
|
+
hand before `.count()` works against it; until then it fails loudly, naming the method.
|
|
18
|
+
|
|
3
19
|
## 2.18.1
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -381,7 +381,7 @@ The query DSL is schema-parameterized: you define your own SHACL shapes, and Lin
|
|
|
381
381
|
- `and(...)` / `or(...)` combinations
|
|
382
382
|
- Set filtering with `some(...)` / `every(...)` (and implicit `some`)
|
|
383
383
|
- Outer `where(...)` chaining
|
|
384
|
-
- Counting
|
|
384
|
+
- Counting: `.count()` for how many instances match, `.size()` for a property's value count
|
|
385
385
|
- Custom result formats (object mapping)
|
|
386
386
|
- Computed values — derive new fields with arithmetic, string, date, and comparison methods
|
|
387
387
|
- Expression-based WHERE filters (`p.name.strlen().gt(5)`)
|
|
@@ -487,12 +487,33 @@ const outer = await Person.select((p) => p.knows).where((p) =>
|
|
|
487
487
|
);
|
|
488
488
|
```
|
|
489
489
|
|
|
490
|
-
#### Counting
|
|
490
|
+
#### Counting
|
|
491
|
+
|
|
492
|
+
Two different questions, two different queries.
|
|
493
|
+
|
|
494
|
+
`.count()` answers **how many instances match** — one number for the whole match set. It respects
|
|
495
|
+
`where` and `minus`, and ignores `limit`/`offset`, so a paged table can ask for its total row count
|
|
496
|
+
beside the page it is showing:
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
/* Result: number */
|
|
500
|
+
const total = await Person.count();
|
|
501
|
+
const matching = await Person.select().where((p) => p.name.equals('Semmy')).count();
|
|
502
|
+
|
|
503
|
+
// Lowers to: SELECT (COUNT(DISTINCT ?a0) AS ?count) WHERE { ?a0 a Person ; name ?n . FILTER(…) }
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
`.size()` answers **how many values a property has**, per row:
|
|
507
|
+
|
|
491
508
|
```typescript
|
|
492
509
|
/* Result: Array<{id: string; knows: number}> */
|
|
493
|
-
const
|
|
510
|
+
const perPerson = await Person.select((p) => p.knows.size());
|
|
494
511
|
```
|
|
495
512
|
|
|
513
|
+
A count resolves to a real number and **rejects on failure** — it never reports an unreachable store
|
|
514
|
+
as `0`. `.toCount()` gives you the count query itself, for a router that forwards
|
|
515
|
+
(`builder.toCount().toJSON()`) rather than executes.
|
|
516
|
+
|
|
496
517
|
#### Custom result formats
|
|
497
518
|
```typescript
|
|
498
519
|
/* Result: Array<{id: string; nameIsMoa: boolean; numFriends: number}> */
|
package/lib/esm/index.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ export type { QueryBuilderJSON } from './queries/QueryBuilder.js';
|
|
|
20
20
|
export { AskBuilder, isAskQuery } from './queries/AskBuilder.js';
|
|
21
21
|
export type { AskSpec } from './queries/AskBuilder.js';
|
|
22
22
|
export type { AskQuery, AskQueryJSON, RawAskInput } from './queries/AskQuery.js';
|
|
23
|
+
export { CountBuilder, isCountQuery } from './queries/CountBuilder.js';
|
|
24
|
+
export type { CountSpec } from './queries/CountBuilder.js';
|
|
25
|
+
export type { CountQuery, CountQueryJSON, RawCountInput } from './queries/CountQuery.js';
|
|
23
26
|
export { CreateBuilder } from './queries/CreateBuilder.js';
|
|
24
27
|
export { UpdateBuilder } from './queries/UpdateBuilder.js';
|
|
25
28
|
export { DeleteBuilder } from './queries/DeleteBuilder.js';
|
package/lib/esm/index.js
CHANGED
|
@@ -22,6 +22,7 @@ export { PropertyPath, walkPropertyPath } from './queries/PropertyPath.js';
|
|
|
22
22
|
export { FieldSet } from './queries/FieldSet.js';
|
|
23
23
|
// Phase 3b — Mutation builders
|
|
24
24
|
export { AskBuilder, isAskQuery } from './queries/AskBuilder.js';
|
|
25
|
+
export { CountBuilder, isCountQuery } from './queries/CountBuilder.js';
|
|
25
26
|
export { CreateBuilder } from './queries/CreateBuilder.js';
|
|
26
27
|
export { UpdateBuilder } from './queries/UpdateBuilder.js';
|
|
27
28
|
export { DeleteBuilder } from './queries/DeleteBuilder.js';
|
package/lib/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAC,UAAU,EAAE,SAAS,EAAC,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAC,OAAO,EAAC,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAC,uBAAuB,EAAC,MAAM,qCAAqC,CAAC;AAC5E,sEAAsE;AACtE,OAAO,EAAC,KAAK,EAAC,MAAM,mBAAmB,CAAC;AACxC,kEAAkE;AAClE,OAAO,EAAC,QAAQ,EAAE,WAAW,EAAE,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAQnF,OAAO,EAAC,aAAa,EAAC,MAAM,0BAA0B,CAAC;AACvD,2CAA2C;AAC3C,OAAO,EAAC,aAAa,EAAE,YAAY,EAAC,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAC,KAAK,EAAC,MAAM,oBAAoB,CAAC;AAEzC,OAAO,EAAC,QAAQ,EAAC,MAAM,uBAAuB,CAAC;AAE/C,OAAO,EACL,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAC,YAAY,EAAE,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AAEzE,sBAAsB;AACtB,OAAO,EAAC,QAAQ,EAAC,MAAM,uBAAuB,CAAC;AAM/C,+BAA+B;AAC/B,OAAO,EAAC,UAAU,EAAE,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAG/D,OAAO,EAAC,aAAa,EAAC,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAC,aAAa,EAAC,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAC,aAAa,EAAC,MAAM,4BAA4B,CAAC;AAGzD,8CAA8C;AAC9C,OAAO,EAAC,cAAc,EAAC,MAAM,iCAAiC,CAAC;AAE/D,OAAO,EAAC,IAAI,EAAC,MAAM,uBAAuB,CAAC;AAe3C,sEAAsE;AACtE,OAAO,EACL,cAAc,EACd,WAAW,GACZ,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,cAAc,EACd,iBAAiB,GAClB,MAAM,gCAAgC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAC,UAAU,EAAE,SAAS,EAAC,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAC,OAAO,EAAC,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAC,uBAAuB,EAAC,MAAM,qCAAqC,CAAC;AAC5E,sEAAsE;AACtE,OAAO,EAAC,KAAK,EAAC,MAAM,mBAAmB,CAAC;AACxC,kEAAkE;AAClE,OAAO,EAAC,QAAQ,EAAE,WAAW,EAAE,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AAQnF,OAAO,EAAC,aAAa,EAAC,MAAM,0BAA0B,CAAC;AACvD,2CAA2C;AAC3C,OAAO,EAAC,aAAa,EAAE,YAAY,EAAC,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAC,KAAK,EAAC,MAAM,oBAAoB,CAAC;AAEzC,OAAO,EAAC,QAAQ,EAAC,MAAM,uBAAuB,CAAC;AAE/C,OAAO,EACL,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAC,YAAY,EAAE,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AAEzE,sBAAsB;AACtB,OAAO,EAAC,QAAQ,EAAC,MAAM,uBAAuB,CAAC;AAM/C,+BAA+B;AAC/B,OAAO,EAAC,UAAU,EAAE,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAG/D,OAAO,EAAC,YAAY,EAAE,YAAY,EAAC,MAAM,2BAA2B,CAAC;AAGrE,OAAO,EAAC,aAAa,EAAC,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAC,aAAa,EAAC,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAC,aAAa,EAAC,MAAM,4BAA4B,CAAC;AAGzD,8CAA8C;AAC9C,OAAO,EAAC,cAAc,EAAC,MAAM,iCAAiC,CAAC;AAE/D,OAAO,EAAC,IAAI,EAAC,MAAM,uBAAuB,CAAC;AAe3C,sEAAsE;AACtE,OAAO,EACL,cAAc,EACd,WAAW,GACZ,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACL,cAAc,EACd,iBAAiB,GAClB,MAAM,gCAAgC,CAAC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SelectQuery } from '../queries/SelectQuery.js';
|
|
2
2
|
import type { AskQuery } from '../queries/AskQuery.js';
|
|
3
|
+
import type { CountQuery } from '../queries/CountQuery.js';
|
|
3
4
|
import type { CreateQuery } from '../queries/CreateQuery.js';
|
|
4
5
|
import type { UpdateQuery } from '../queries/UpdateQuery.js';
|
|
5
6
|
import type { DeleteQuery, DeleteResponse } from '../queries/DeleteQuery.js';
|
|
@@ -42,6 +43,28 @@ export interface IDataset {
|
|
|
42
43
|
* API was built to remove.
|
|
43
44
|
*/
|
|
44
45
|
askQuery(query: AskQuery): Promise<boolean>;
|
|
46
|
+
/**
|
|
47
|
+
* Count the matching instances — a number, not a result set.
|
|
48
|
+
*
|
|
49
|
+
* **Optional**, unlike {@link askQuery}. Adding a required method would break
|
|
50
|
+
* every existing implementer at compile time; `resolveCount` in `queryDispatch`
|
|
51
|
+
* turns a missing implementation into a precise runtime error instead. Every
|
|
52
|
+
* store extending {@link SparqlDataset} gets it with no edit.
|
|
53
|
+
*
|
|
54
|
+
* A {@link CountQuery} carries a pattern and nothing else: no projection, no
|
|
55
|
+
* sorting, and in particular no pagination — a count of a windowed query is
|
|
56
|
+
* meaningless, so the type cannot hold a window. A SPARQL-backed store emits
|
|
57
|
+
* `SELECT (COUNT(DISTINCT ?s) AS ?count) WHERE { … }`; another backend answers it
|
|
58
|
+
* however it can. This package contains **no path that rewrites a count as a
|
|
59
|
+
* select** and measures the array: that would hide an unbounded read behind a
|
|
60
|
+
* call that looks cheap.
|
|
61
|
+
*
|
|
62
|
+
* Must resolve to a real, non-negative integer — a non-number is rejected, not
|
|
63
|
+
* coerced. Must reject on failure: reporting an unreachable store as `0` renders
|
|
64
|
+
* an empty table that is indistinguishable from real data, which is the failure
|
|
65
|
+
* mode this API was built to remove.
|
|
66
|
+
*/
|
|
67
|
+
countQuery?(query: CountQuery): Promise<number>;
|
|
45
68
|
/**
|
|
46
69
|
* Receives update AND upsert mutations — `lower(query)` yields `kind: 'update'`,
|
|
47
70
|
* `'update_where'` or `'upsert'`. An implementation that does not handle `'upsert'`
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `CountBuilder` — a query whose answer is a number.
|
|
3
|
+
*
|
|
4
|
+
* A small sibling of `SelectBuilder` rather than a mode of it, for the same reason
|
|
5
|
+
* {@link AskBuilder} is: it can only hold a pattern (shape, subject(s), where,
|
|
6
|
+
* minus), so the normalisation a count needs is a property of the *type*. There is
|
|
7
|
+
* no projection, ordering or **pagination** to drop, because none can be set — and
|
|
8
|
+
* a count of a windowed query is meaningless, so that is the point.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately **non-generic**. `SelectBuilder<S, R, Result>` is heavily inferred;
|
|
11
|
+
* a count answers `number` no matter what the select's projection was, so this type
|
|
12
|
+
* takes part in none of that inference and cannot perturb it.
|
|
13
|
+
*/
|
|
14
|
+
import type { ShapeConstructor } from '../shapes/Shape.js';
|
|
15
|
+
import type { WherePath } from './SelectQuery.js';
|
|
16
|
+
import type { RawMinusEntry } from './IRDesugar.js';
|
|
17
|
+
import type { NodeShapeData } from '../shapes/SHACL.js';
|
|
18
|
+
import type { NodeReferenceValue } from './QueryFactory.js';
|
|
19
|
+
import type { IDataset } from '../interfaces/IDataset.js';
|
|
20
|
+
import { PendingQueryContext } from './QueryContext.js';
|
|
21
|
+
import type { CountQuery, CountQueryJSON, RawCountInput } from './CountQuery.js';
|
|
22
|
+
/**
|
|
23
|
+
* Everything a count can carry. Assembled by `SelectBuilder.toCount()` or
|
|
24
|
+
* `Shape.count()`.
|
|
25
|
+
*
|
|
26
|
+
* `shapeClass` is required — a shapeless count would count every node in the store.
|
|
27
|
+
*/
|
|
28
|
+
export type CountSpec = {
|
|
29
|
+
shapeClass: ShapeConstructor<any>;
|
|
30
|
+
subject?: NodeReferenceValue | PendingQueryContext;
|
|
31
|
+
subjects?: NodeReferenceValue[];
|
|
32
|
+
where?: WherePath;
|
|
33
|
+
minusEntries?: RawMinusEntry[];
|
|
34
|
+
/** `.for(null)` was called — there is no subject to count. */
|
|
35
|
+
nullSubject?: boolean;
|
|
36
|
+
};
|
|
37
|
+
export declare class CountBuilder implements PromiseLike<number>, Promise<number> {
|
|
38
|
+
private readonly _spec;
|
|
39
|
+
private constructor();
|
|
40
|
+
/** Build from an assembled spec — used by `.toCount()` and `Shape.count()`. */
|
|
41
|
+
static of(spec: CountSpec): CountBuilder;
|
|
42
|
+
/** Discriminator for the free `lower()` function and dataset routing. */
|
|
43
|
+
readonly __queryKind: "count";
|
|
44
|
+
/** The shape whose matching instances are counted — also the routing key. */
|
|
45
|
+
get shape(): NodeShapeData;
|
|
46
|
+
toRawInput(): RawCountInput;
|
|
47
|
+
toJSON(): CountQueryJSON;
|
|
48
|
+
static fromJSON(json: CountQueryJSON): CountBuilder;
|
|
49
|
+
/**
|
|
50
|
+
* Execute and resolve to a real number.
|
|
51
|
+
*
|
|
52
|
+
* **A failure rejects — it is never reported as `0`.** A count of `0` renders an
|
|
53
|
+
* empty table and is indistinguishable from real data, so a broken count that
|
|
54
|
+
* answered `0` would hide rather than fail. Do not wrap this in `.catch(() => 0)`.
|
|
55
|
+
*
|
|
56
|
+
* The one case that resolves `0` without querying is a query with no subject to
|
|
57
|
+
* count — `.for(null)`, or a `PendingQueryContext` as the subject that has not
|
|
58
|
+
* landed: "how many nodes with no id match?" has a correct total answer, and it
|
|
59
|
+
* is `0`.
|
|
60
|
+
*/
|
|
61
|
+
exec(target?: IDataset): Promise<number>;
|
|
62
|
+
/** `await` triggers execution. */
|
|
63
|
+
then<TResult1 = number, TResult2 = never>(onfulfilled?: ((value: number) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
|
|
64
|
+
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null): Promise<number | TResult>;
|
|
65
|
+
finally(onfinally?: (() => void) | null): Promise<number>;
|
|
66
|
+
get [Symbol.toStringTag](): string;
|
|
67
|
+
}
|
|
68
|
+
/** Narrow an unknown query object to a count. */
|
|
69
|
+
export declare function isCountQuery(query: unknown): query is CountQuery;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
5
|
+
*/
|
|
6
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
7
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
8
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
9
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
10
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
11
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
12
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
import { resolveShape } from './resolveShape.js';
|
|
16
|
+
import { PendingQueryContext } from './QueryContext.js';
|
|
17
|
+
import { encodeContextRef, isContextRefJSON } from './ContextRef.js';
|
|
18
|
+
import { WIRE_VERSION, assertWireVersion } from './wireVersion.js';
|
|
19
|
+
import { getQueryDispatch, resolveCount } from './queryDispatch.js';
|
|
20
|
+
import { serializeWherePath, serializeRawMinusEntry, deserializeWherePath, deserializeRawMinusEntry, } from './QueryBuilderSerialization.js';
|
|
21
|
+
export class CountBuilder {
|
|
22
|
+
constructor(spec) {
|
|
23
|
+
/** Discriminator for the free `lower()` function and dataset routing. */
|
|
24
|
+
this.__queryKind = 'count';
|
|
25
|
+
this._spec = spec;
|
|
26
|
+
}
|
|
27
|
+
/** Build from an assembled spec — used by `.toCount()` and `Shape.count()`. */
|
|
28
|
+
static of(spec) {
|
|
29
|
+
return new CountBuilder(spec);
|
|
30
|
+
}
|
|
31
|
+
/** The shape whose matching instances are counted — also the routing key. */
|
|
32
|
+
get shape() {
|
|
33
|
+
return this._spec.shapeClass.shape;
|
|
34
|
+
}
|
|
35
|
+
toRawInput() {
|
|
36
|
+
const { shapeClass, subject, subjects, where, minusEntries, nullSubject } = this._spec;
|
|
37
|
+
const input = { shape: shapeClass };
|
|
38
|
+
// Carried so lowering can reject it. `exec()` answers `0` before dispatching,
|
|
39
|
+
// but a store handed the builder directly (e.g. after `fromJSON`) would
|
|
40
|
+
// otherwise lower a subject-less query that matches every instance of the
|
|
41
|
+
// shape and report that as the count of one node.
|
|
42
|
+
if (nullSubject)
|
|
43
|
+
input.nullSubject = true;
|
|
44
|
+
if (subject)
|
|
45
|
+
input.subject = subject;
|
|
46
|
+
if (subjects && subjects.length > 0)
|
|
47
|
+
input.subjects = subjects;
|
|
48
|
+
if (where)
|
|
49
|
+
input.where = where;
|
|
50
|
+
if (minusEntries && minusEntries.length > 0)
|
|
51
|
+
input.minusEntries = minusEntries;
|
|
52
|
+
return input;
|
|
53
|
+
}
|
|
54
|
+
toJSON() {
|
|
55
|
+
var _a;
|
|
56
|
+
const { shapeClass, subject, subjects, where, minusEntries, nullSubject } = this._spec;
|
|
57
|
+
const shapeId = (_a = shapeClass.shape) === null || _a === void 0 ? void 0 : _a.id;
|
|
58
|
+
if (!shapeId) {
|
|
59
|
+
// Refuse here rather than emitting `shape: ''` for the receiver's `fromJSON`
|
|
60
|
+
// to reject: the caller who holds the shape can act on this, and a peer
|
|
61
|
+
// across a wire cannot.
|
|
62
|
+
throw new Error('Cannot serialize a count query whose shape has no id. A count envelope must ' +
|
|
63
|
+
'name a shape — a shapeless count would count every node in the store.');
|
|
64
|
+
}
|
|
65
|
+
const json = {
|
|
66
|
+
v: WIRE_VERSION,
|
|
67
|
+
op: 'count',
|
|
68
|
+
shape: shapeId,
|
|
69
|
+
};
|
|
70
|
+
if (subject instanceof PendingQueryContext) {
|
|
71
|
+
// Carry the reference, not its (possibly unresolved) id, so the receiver
|
|
72
|
+
// resolves it against its own context map at lowering time.
|
|
73
|
+
json.subject = encodeContextRef(subject.contextName);
|
|
74
|
+
}
|
|
75
|
+
else if (subject && typeof subject === 'object' && 'id' in subject) {
|
|
76
|
+
json.subject = subject.id;
|
|
77
|
+
}
|
|
78
|
+
if (subjects && subjects.length > 0) {
|
|
79
|
+
json.subjects = subjects.map((s) => s.id);
|
|
80
|
+
}
|
|
81
|
+
if (where) {
|
|
82
|
+
json.where = serializeWherePath(where, shapeClass.shape);
|
|
83
|
+
}
|
|
84
|
+
if (minusEntries && minusEntries.length > 0) {
|
|
85
|
+
json.minusEntries = minusEntries.map((e) => serializeRawMinusEntry(e, shapeClass.shape));
|
|
86
|
+
}
|
|
87
|
+
if (nullSubject) {
|
|
88
|
+
json.nullSubject = true;
|
|
89
|
+
}
|
|
90
|
+
return json;
|
|
91
|
+
}
|
|
92
|
+
static fromJSON(json) {
|
|
93
|
+
var _a;
|
|
94
|
+
assertWireVersion(json.v);
|
|
95
|
+
if (!json.shape) {
|
|
96
|
+
throw new Error('A count envelope must name a `shape`. A shapeless count would count every ' +
|
|
97
|
+
'node in the store, under any type or none.');
|
|
98
|
+
}
|
|
99
|
+
const shapeClass = resolveShape(json.shape);
|
|
100
|
+
const spec = { shapeClass };
|
|
101
|
+
if (json.subject !== undefined) {
|
|
102
|
+
spec.subject = isContextRefJSON(json.subject)
|
|
103
|
+
? new PendingQueryContext(json.subject['@ctx'])
|
|
104
|
+
: { id: json.subject };
|
|
105
|
+
}
|
|
106
|
+
if (json.subjects && json.subjects.length > 0) {
|
|
107
|
+
spec.subjects = json.subjects.map((id) => ({ id }));
|
|
108
|
+
}
|
|
109
|
+
if (json.where) {
|
|
110
|
+
spec.where = deserializeWherePath(shapeClass.shape, json.where);
|
|
111
|
+
}
|
|
112
|
+
if ((_a = json.minusEntries) === null || _a === void 0 ? void 0 : _a.length) {
|
|
113
|
+
spec.minusEntries = json.minusEntries.map((e) => deserializeRawMinusEntry(shapeClass.shape, e));
|
|
114
|
+
}
|
|
115
|
+
if (json.nullSubject)
|
|
116
|
+
spec.nullSubject = true;
|
|
117
|
+
return new CountBuilder(spec);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Execute and resolve to a real number.
|
|
121
|
+
*
|
|
122
|
+
* **A failure rejects — it is never reported as `0`.** A count of `0` renders an
|
|
123
|
+
* empty table and is indistinguishable from real data, so a broken count that
|
|
124
|
+
* answered `0` would hide rather than fail. Do not wrap this in `.catch(() => 0)`.
|
|
125
|
+
*
|
|
126
|
+
* The one case that resolves `0` without querying is a query with no subject to
|
|
127
|
+
* count — `.for(null)`, or a `PendingQueryContext` as the subject that has not
|
|
128
|
+
* landed: "how many nodes with no id match?" has a correct total answer, and it
|
|
129
|
+
* is `0`.
|
|
130
|
+
*/
|
|
131
|
+
exec(target) {
|
|
132
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
133
|
+
const { nullSubject, subject } = this._spec;
|
|
134
|
+
if (nullSubject)
|
|
135
|
+
return 0;
|
|
136
|
+
if (subject instanceof PendingQueryContext && !subject.id)
|
|
137
|
+
return 0;
|
|
138
|
+
// `async` ensures a missing global dispatch rejects rather than throwing
|
|
139
|
+
// synchronously past the caller's `.catch()`.
|
|
140
|
+
const dispatch = target !== null && target !== void 0 ? target : getQueryDispatch();
|
|
141
|
+
return resolveCount(dispatch, this);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/** `await` triggers execution. */
|
|
145
|
+
then(onfulfilled, onrejected) {
|
|
146
|
+
return this.exec().then(onfulfilled, onrejected);
|
|
147
|
+
}
|
|
148
|
+
catch(onrejected) {
|
|
149
|
+
return this.then().catch(onrejected);
|
|
150
|
+
}
|
|
151
|
+
finally(onfinally) {
|
|
152
|
+
return this.then().finally(onfinally);
|
|
153
|
+
}
|
|
154
|
+
get [Symbol.toStringTag]() {
|
|
155
|
+
return 'CountBuilder';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Narrow an unknown query object to a count. */
|
|
159
|
+
export function isCountQuery(query) {
|
|
160
|
+
return !!query && query.__queryKind === 'count';
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=CountBuilder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CountBuilder.js","sourceRoot":"","sources":["../../../src/queries/CountBuilder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The count query — "how many instances match?", answered with a number.
|
|
3
|
+
*
|
|
4
|
+
* A count carries a **pattern and nothing else**: no projection, no sorting, no
|
|
5
|
+
* pagination. Those shape or window a *solution sequence*, and the number a count
|
|
6
|
+
* answers is a property of the whole match set, not of a page of it — so rather
|
|
7
|
+
* than being ignored or guarded against, they are simply not expressible here.
|
|
8
|
+
*
|
|
9
|
+
* This is the sibling of {@link AskQuery}: same idea, `number` instead of
|
|
10
|
+
* `boolean`. `SelectBuilder.count()` is the first question asked this way.
|
|
11
|
+
*/
|
|
12
|
+
import type { NodeShapeData } from '../shapes/SHACL.js';
|
|
13
|
+
import type { WherePath } from './SelectQuery.js';
|
|
14
|
+
import type { RawMinusEntry } from './IRDesugar.js';
|
|
15
|
+
import type { NodeReferenceValue } from './QueryFactory.js';
|
|
16
|
+
import type { PendingQueryContext } from './QueryContext.js';
|
|
17
|
+
import type { ContextRefJSON } from './ContextRef.js';
|
|
18
|
+
import type { WherePathJSON, RawMinusEntryJSON } from './QueryBuilderSerialization.js';
|
|
19
|
+
/**
|
|
20
|
+
* The live, closed (read-only) count query — the object an `IDataset` receives.
|
|
21
|
+
*
|
|
22
|
+
* `shape` is **required**, unlike an ask's. A shapeless count would count every
|
|
23
|
+
* node in the store; there is no useful reading of that, so the type does not
|
|
24
|
+
* permit it.
|
|
25
|
+
*/
|
|
26
|
+
export interface CountQuery {
|
|
27
|
+
readonly __queryKind: 'count';
|
|
28
|
+
/** The shape whose matching instances are counted. Also the routing key. */
|
|
29
|
+
readonly shape: NodeShapeData;
|
|
30
|
+
toJSON(): CountQueryJSON;
|
|
31
|
+
toRawInput(): RawCountInput;
|
|
32
|
+
}
|
|
33
|
+
/** Pre-lowering input, as the builder hands it to `lower()`. */
|
|
34
|
+
export type RawCountInput = {
|
|
35
|
+
/**
|
|
36
|
+
* `.for(null)` — no subject to count. Lowering rejects it; `exec()` answers `0`
|
|
37
|
+
* before dispatching. See `lowerCount`.
|
|
38
|
+
*/
|
|
39
|
+
nullSubject?: boolean;
|
|
40
|
+
shape?: {
|
|
41
|
+
shape?: {
|
|
42
|
+
id?: string;
|
|
43
|
+
};
|
|
44
|
+
id?: string;
|
|
45
|
+
};
|
|
46
|
+
subject?: NodeReferenceValue | PendingQueryContext;
|
|
47
|
+
subjects?: NodeReferenceValue[];
|
|
48
|
+
where?: WherePath;
|
|
49
|
+
minusEntries?: RawMinusEntry[];
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The DSL-JSON wire form of a count query.
|
|
53
|
+
*
|
|
54
|
+
* Carries `op: 'count'` — the same discriminator ask and mutation envelopes use, so
|
|
55
|
+
* `fromJSON` routes on it and an older peer rejects an unknown op loudly rather
|
|
56
|
+
* than reinterpreting the envelope as a select (which would answer with *rows*,
|
|
57
|
+
* windowed by nothing, where the caller expected a total).
|
|
58
|
+
*
|
|
59
|
+
* Every field is pattern-bearing. There is deliberately no `fields`, `limit`,
|
|
60
|
+
* `offset`, `sortBy` or `one`: a receiver has nothing to validate or ignore.
|
|
61
|
+
*
|
|
62
|
+
* ```json
|
|
63
|
+
* {"v": "1.1", "op": "count",
|
|
64
|
+
* "shape": "https://linked.cm/shape/core/Person",
|
|
65
|
+
* "where": {"name": "Semmy"}}
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export type CountQueryJSON = {
|
|
69
|
+
v?: string;
|
|
70
|
+
op: 'count';
|
|
71
|
+
/** Required — see {@link CountQuery.shape}. */
|
|
72
|
+
shape: string;
|
|
73
|
+
/** A node id, or a `{@ctx: name}` reference resolved at lowering. */
|
|
74
|
+
subject?: string | ContextRefJSON;
|
|
75
|
+
subjects?: string[];
|
|
76
|
+
where?: WherePathJSON;
|
|
77
|
+
minusEntries?: RawMinusEntryJSON[];
|
|
78
|
+
/** `.for(null)` — no subject to count; the answer is `0` without querying. */
|
|
79
|
+
nullSubject?: boolean;
|
|
80
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CountQuery.js","sourceRoot":"","sources":["../../../src/queries/CountQuery.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
|
@@ -3,7 +3,7 @@ import type { PathExpr } from '../paths/PropertyPathExpr.js';
|
|
|
3
3
|
export type IRDirection = 'ASC' | 'DESC';
|
|
4
4
|
export type IRAlias = string;
|
|
5
5
|
export type IRValue = string | number | boolean | null;
|
|
6
|
-
export type IRQuery = IRSelectQuery | IRAskQuery | IRCreateMutation | IRUpdateMutation | IRUpsertMutation | IRDeleteMutation | IRDeleteAllMutation | IRDeleteWhereMutation | IRUpdateWhereMutation;
|
|
6
|
+
export type IRQuery = IRSelectQuery | IRAskQuery | IRCountQuery | IRCreateMutation | IRUpdateMutation | IRUpsertMutation | IRDeleteMutation | IRDeleteAllMutation | IRDeleteWhereMutation | IRUpdateWhereMutation;
|
|
7
7
|
export type IRSelectQuery = {
|
|
8
8
|
kind: 'select';
|
|
9
9
|
root: IRShapeScanPattern;
|
|
@@ -39,6 +39,36 @@ export type IRAskQuery = {
|
|
|
39
39
|
subjectId?: string;
|
|
40
40
|
subjectIds?: string[];
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* A query whose answer is a single number: how many instances of a shape match.
|
|
44
|
+
*
|
|
45
|
+
* Like {@link IRAskQuery}, it carries a **pattern and nothing else**. There is no
|
|
46
|
+
* `projection`, `orderBy`, `limit` or `offset`: each of those shapes or windows a
|
|
47
|
+
* *solution sequence*, and a count has none — the number it answers is a property
|
|
48
|
+
* of the whole match set. So rather than being ignored during conversion, or
|
|
49
|
+
* guarded against, they are unrepresentable.
|
|
50
|
+
*
|
|
51
|
+
* That is the entire reason this is a distinct IR kind and not a flag on
|
|
52
|
+
* {@link IRSelectQuery}: a count of a windowed query is meaningless, and a type
|
|
53
|
+
* that cannot hold the window cannot half-drop it.
|
|
54
|
+
*
|
|
55
|
+
* `root` is **required**, unlike an ask's. A shapeless count would count every
|
|
56
|
+
* node in the store, which is never what a caller means.
|
|
57
|
+
*/
|
|
58
|
+
export type IRCountQuery = {
|
|
59
|
+
kind: 'count';
|
|
60
|
+
root: IRShapeScanPattern;
|
|
61
|
+
patterns: IRGraphPattern[];
|
|
62
|
+
where?: IRExpression;
|
|
63
|
+
subjectId?: string;
|
|
64
|
+
subjectIds?: string[];
|
|
65
|
+
/**
|
|
66
|
+
* The variable the `COUNT` is bound to (`(COUNT(DISTINCT ?a0) AS ?count)`).
|
|
67
|
+
* Carried in the IR rather than hardcoded in two places so lowering and result
|
|
68
|
+
* mapping cannot disagree about it.
|
|
69
|
+
*/
|
|
70
|
+
alias: IRAlias;
|
|
71
|
+
};
|
|
42
72
|
export type IRProjectionItem = {
|
|
43
73
|
alias: IRAlias;
|
|
44
74
|
expression: IRExpression;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Shape, type ShapeConstructor } from '../shapes/Shape.js';
|
|
2
2
|
import { type QueryBuildFn, type WhereClause, type QueryResponseToResultType, type SelectAllQueryResponse, type QueryComponentLike } from './SelectQuery.js';
|
|
3
3
|
import type { RawSelectInput } from './IRDesugar.js';
|
|
4
|
+
import { CountBuilder } from './CountBuilder.js';
|
|
4
5
|
import type { IDataset } from '../interfaces/IDataset.js';
|
|
5
6
|
import type { NodeShapeData } from '../shapes/SHACL.js';
|
|
6
7
|
import type { NodeReferenceValue } from './QueryFactory.js';
|
|
@@ -156,6 +157,85 @@ export declare class SelectBuilder<S extends Shape = Shape, R = any, Result = an
|
|
|
156
157
|
* @param target Optional explicit dataset, as for {@link exec}.
|
|
157
158
|
*/
|
|
158
159
|
exists(target?: IDataset): Promise<boolean>;
|
|
160
|
+
/**
|
|
161
|
+
* **How many** rows match this query — the total of the match set, as a real
|
|
162
|
+
* `number`.
|
|
163
|
+
*
|
|
164
|
+
* ```ts
|
|
165
|
+
* await Person.select().where(p => p.name.equals('Semmy')).count(); // number
|
|
166
|
+
* await SelectBuilder.from(shapeIri).where(…).count(); // from an IRI alone
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* ### The query is normalised to its cheapest correct form first
|
|
170
|
+
*
|
|
171
|
+
* **Dropped** — the projection, preloads, sorting *and pagination*
|
|
172
|
+
* (`limit`/`offset`). **Kept** — filters, `minus` entries and the subject: those
|
|
173
|
+
* decide membership of the match set. Against a SPARQL store that goes out as:
|
|
174
|
+
*
|
|
175
|
+
* ```sparql
|
|
176
|
+
* SELECT (COUNT(DISTINCT ?a0) AS ?count)
|
|
177
|
+
* WHERE { ?a0 rdf:type <…> . ?a0 <…name> ?a0_name . FILTER(?a0_name = "Semmy") }
|
|
178
|
+
* ```
|
|
179
|
+
*
|
|
180
|
+
* ### `limit`/`offset` are dropped, not rejected
|
|
181
|
+
*
|
|
182
|
+
* A count of a windowed query is meaningless — `OFFSET` skips rows of a *solution
|
|
183
|
+
* sequence*, and the number a count answers is a property of the whole match set,
|
|
184
|
+
* not of a page of it. So `.limit(20).offset(40).count()` costs, and answers,
|
|
185
|
+
* exactly the same as a bare `.count()`.
|
|
186
|
+
*
|
|
187
|
+
* Dropping rather than throwing is deliberate: the paging caller holds **one**
|
|
188
|
+
* builder and wants the page *and* its total from the same filter, so rejecting
|
|
189
|
+
* the combination would force it to rebuild the builder by hand for no
|
|
190
|
+
* correctness gain. There is exactly one sensible reading, and it is this one.
|
|
191
|
+
* `.exists()` already made the same call, so the DSL has one rule — *a
|
|
192
|
+
* scalar-answering query drops the solution-sequence modifiers* — not two.
|
|
193
|
+
*
|
|
194
|
+
* Nor is the drop merely a convention: a {@link CountBuilder} has nowhere to hold
|
|
195
|
+
* a window, so it cannot be forgotten or half-applied downstream.
|
|
196
|
+
*
|
|
197
|
+
* `DISTINCT` is likewise not optional. A filter on a multi-valued property yields
|
|
198
|
+
* several rows per subject, so the count is of distinct subjects — "how many
|
|
199
|
+
* instances match", which is the question asked.
|
|
200
|
+
*
|
|
201
|
+
* ### Errors are not swallowed
|
|
202
|
+
*
|
|
203
|
+
* A store, transport or lowering failure **rejects**; it is never reported as `0`.
|
|
204
|
+
* `0` is a plausible count: it renders an empty table and looks like data. Do not
|
|
205
|
+
* wrap this in `.catch(() => 0)`.
|
|
206
|
+
*
|
|
207
|
+
* The one case that resolves `0` without querying is a query with no subject to
|
|
208
|
+
* count — `.for(null)`, `.for(undefined)`, or an unresolved `PendingQueryContext`
|
|
209
|
+
* *as the subject*.
|
|
210
|
+
*
|
|
211
|
+
* @param target Optional explicit dataset, as for {@link exec}.
|
|
212
|
+
*/
|
|
213
|
+
count(target?: IDataset): Promise<number>;
|
|
214
|
+
/**
|
|
215
|
+
* Reduce this select to the count query that answers "how many match?".
|
|
216
|
+
*
|
|
217
|
+
* Public, unlike the ask equivalent, because a caller may want to **forward**
|
|
218
|
+
* rather than execute: `builder.toCount().toJSON()` is the `{op: 'count'}` wire
|
|
219
|
+
* envelope a router or RPC boundary sends on, and `fromJSON` turns it back into a
|
|
220
|
+
* `CountBuilder` on the other side.
|
|
221
|
+
*
|
|
222
|
+
* Only the pattern survives — shape, subject(s), filters, `minus`. The projection,
|
|
223
|
+
* preloads, sorting and pagination are not so much "dropped" as unrepresentable:
|
|
224
|
+
* {@link CountBuilder} has nowhere to put them. That is the point of it being a
|
|
225
|
+
* separate builder rather than a mode of this one.
|
|
226
|
+
*/
|
|
227
|
+
toCount(): CountBuilder;
|
|
228
|
+
/**
|
|
229
|
+
* The pattern-bearing part of this select: shape-membership, subject(s), filters
|
|
230
|
+
* and `minus` — everything that decides which nodes are in the match set, and
|
|
231
|
+
* nothing that shapes or windows the rows describing them.
|
|
232
|
+
*
|
|
233
|
+
* Shared by {@link _toAsk} and {@link toCount} rather than written twice. That is
|
|
234
|
+
* not only about duplication: if the two normalisations drifted, `.exists()` and
|
|
235
|
+
* `.count()` would disagree about what the match set *is* — a count of 0 next to
|
|
236
|
+
* an `exists` of `true`, from the same builder.
|
|
237
|
+
*/
|
|
238
|
+
private _patternSpec;
|
|
159
239
|
/**
|
|
160
240
|
* Reduce this select to the ask query that answers the same existence question.
|
|
161
241
|
*
|