@_linked/core 2.18.0 → 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.
Files changed (44) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +24 -3
  3. package/lib/esm/index.d.ts +3 -0
  4. package/lib/esm/index.js +1 -0
  5. package/lib/esm/index.js.map +1 -1
  6. package/lib/esm/interfaces/IDataset.d.ts +23 -0
  7. package/lib/esm/queries/CountBuilder.d.ts +69 -0
  8. package/lib/esm/queries/CountBuilder.js +162 -0
  9. package/lib/esm/queries/CountBuilder.js.map +1 -0
  10. package/lib/esm/queries/CountQuery.d.ts +80 -0
  11. package/lib/esm/queries/CountQuery.js +7 -0
  12. package/lib/esm/queries/CountQuery.js.map +1 -0
  13. package/lib/esm/queries/IntermediateRepresentation.d.ts +31 -1
  14. package/lib/esm/queries/QueryBuilder.d.ts +80 -0
  15. package/lib/esm/queries/QueryBuilder.js +98 -13
  16. package/lib/esm/queries/QueryBuilder.js.map +1 -1
  17. package/lib/esm/queries/fromJSON.d.ts +5 -3
  18. package/lib/esm/queries/fromJSON.js +4 -1
  19. package/lib/esm/queries/fromJSON.js.map +1 -1
  20. package/lib/esm/queries/lower.d.ts +17 -2
  21. package/lib/esm/queries/lower.js +61 -0
  22. package/lib/esm/queries/lower.js.map +1 -1
  23. package/lib/esm/queries/queryDispatch.d.ts +35 -0
  24. package/lib/esm/queries/queryDispatch.js +36 -0
  25. package/lib/esm/queries/queryDispatch.js.map +1 -1
  26. package/lib/esm/shapes/Shape.d.ts +32 -0
  27. package/lib/esm/shapes/Shape.js +47 -0
  28. package/lib/esm/shapes/Shape.js.map +1 -1
  29. package/lib/esm/sparql/SparqlDataset.d.ts +11 -0
  30. package/lib/esm/sparql/SparqlDataset.js +19 -2
  31. package/lib/esm/sparql/SparqlDataset.js.map +1 -1
  32. package/lib/esm/sparql/index.d.ts +3 -3
  33. package/lib/esm/sparql/index.js +3 -3
  34. package/lib/esm/sparql/index.js.map +1 -1
  35. package/lib/esm/sparql/irToAlgebra.d.ts +32 -1
  36. package/lib/esm/sparql/irToAlgebra.js +74 -0
  37. package/lib/esm/sparql/irToAlgebra.js.map +1 -1
  38. package/lib/esm/sparql/resultMapping.d.ts +14 -1
  39. package/lib/esm/sparql/resultMapping.js +40 -0
  40. package/lib/esm/sparql/resultMapping.js.map +1 -1
  41. package/lib/esm/utils/ShapeClass.d.ts +1 -1
  42. package/lib/esm/utils/ShapeClass.js +7 -3
  43. package/lib/esm/utils/ShapeClass.js.map +1 -1
  44. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
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
+
19
+ ## 2.18.1
20
+
21
+ ### Patch Changes
22
+
23
+ - [#220](https://github.com/linked-cm/core/pull/220) [`dc1bdbb`](https://github.com/linked-cm/core/commit/dc1bdbb8e966b57f7b2add7050244a0ae0811fef) Thanks [@flyon](https://github.com/flyon)! - Fix `Class extends value undefined` when registering a runtime shape — `getOrCreateShapeAdapter`
24
+ captured `Shape` at module-evaluation time, which could be before `Shape.js` had finished.
25
+
3
26
  ## 2.18.0
4
27
 
5
28
  ### Minor 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 with `.size()`
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 (size)
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 count = await Person.select((p) => p.knows.size());
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}> */
@@ -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';
@@ -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,7 @@
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
+ export {};
7
+ //# sourceMappingURL=CountQuery.js.map
@@ -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
  *