@cavelang/query 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/dist/src/compile.d.ts +34 -0
- package/dist/src/compile.d.ts.map +1 -0
- package/dist/src/compile.js +277 -0
- package/dist/src/compile.js.map +1 -0
- package/dist/src/index.d.ts +16 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +15 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/pattern.d.ts +89 -0
- package/dist/src/pattern.d.ts.map +1 -0
- package/dist/src/pattern.js +164 -0
- package/dist/src/pattern.js.map +1 -0
- package/package.json +45 -0
- package/src/compile.ts +312 -0
- package/src/index.ts +16 -0
- package/src/pattern.ts +206 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @cavelang/query
|
|
2
|
+
|
|
3
|
+
CAVE-Q — the graph-pattern query layer (spec §12), compiled to SQL over an
|
|
4
|
+
open `@cavelang/store`.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { open } from '@cavelang/store'
|
|
8
|
+
import { query } from '@cavelang/query'
|
|
9
|
+
|
|
10
|
+
const store = open('knowledge.db')
|
|
11
|
+
query(store, '?x USES jwt')
|
|
12
|
+
// → [{ bindings: { x: 'auth/middleware' }, row: { … } }, …]
|
|
13
|
+
|
|
14
|
+
query(store, `?cause CAUSE app/crash
|
|
15
|
+
WHERE conf >= 0.7`)
|
|
16
|
+
|
|
17
|
+
query(store, '?x PART-OF monorepo') // inverse verb → same physical query
|
|
18
|
+
query(store, 'terrier EXTENDS+ animal') // transitive
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Pattern language (§12.1)
|
|
22
|
+
|
|
23
|
+
- `?name` variables bind subject, verb, object or attribute value;
|
|
24
|
+
a variable repeated in two positions forces equality (`?x NEEDS ?x`).
|
|
25
|
+
- `_` is a wildcard.
|
|
26
|
+
- `attr: ?v` matches attribute claims.
|
|
27
|
+
- `@ctx` and `#tag[:value]` on the pattern line filter contexts/tags
|
|
28
|
+
(flat `#tag` matches flat tags only, mirroring `store.byTag`).
|
|
29
|
+
- `VERB NOT` matches negated claims; patterns without `NOT` match
|
|
30
|
+
positive ones — direction is always explicit.
|
|
31
|
+
- **Inverse verbs** are valid: `?x PART-OF monorepo` compiles to
|
|
32
|
+
`verb = 'CONTAINS' AND subject = 'monorepo'` with `?x` binding on the
|
|
33
|
+
object side — the same physical query as the forward pattern.
|
|
34
|
+
- `VERB+` is transitive (one or more hops), compiled to a recursive CTE
|
|
35
|
+
over current, positive, non-retracted edges, depth-capped at 32.
|
|
36
|
+
Transitive works through inverses too (`packages/api PART-OF+ ?c` walks
|
|
37
|
+
`CONTAINS` upward from the object side).
|
|
38
|
+
|
|
39
|
+
## Filters (§12.2)
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
WHERE conf >= 0.8 (also accepts 80%)
|
|
43
|
+
WHERE tag = security (bare key matches any value; topic:auth is exact)
|
|
44
|
+
WHERE context = production
|
|
45
|
+
WHERE value > 1000 req/s (numeric column; unit equality when given)
|
|
46
|
+
WHERE tx > 2026-01-01 (dates are whole-day UTC intervals)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`tx` filters compile a date to the interval `[day-start, next-day-start)`
|
|
50
|
+
in UUIDv7 space: `=` means "recorded that day", `<=` includes the boundary
|
|
51
|
+
day that `<` excludes, and `>` starts the day after. A timestamp value
|
|
52
|
+
covers one second.
|
|
53
|
+
|
|
54
|
+
## Semantics
|
|
55
|
+
|
|
56
|
+
- Queries run over **current beliefs** (latest tx per claim key, §9.1) by
|
|
57
|
+
default, and skip retracted (`@ 0%`) ones — a retracted claim has no
|
|
58
|
+
current support (§9.3), and `VERB` must agree with `VERB+` on a one-hop
|
|
59
|
+
path. An explicit `WHERE conf …` filter or `{ all: true }` opts back in.
|
|
60
|
+
- Object-position variables bind relational rows only (`object IS NOT
|
|
61
|
+
NULL`) — `?x ?verb ?y` enumerates the relation graph, not attributes. A
|
|
62
|
+
*bound* date/number object additionally matches metric rows
|
|
63
|
+
(`latency IS 30ms` the pattern finds `latency IS 30ms` the claim).
|
|
64
|
+
- A repeated variable forces equality in transitive patterns too:
|
|
65
|
+
`?x EXTENDS+ ?x` finds nodes on cycles, not every reachable pair.
|
|
66
|
+
- Transitive patterns support endpoint slots only; tag/context/WHERE
|
|
67
|
+
filters on them are rejected rather than silently ignored.
|
|
68
|
+
|
|
69
|
+
## Tests
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
pnpm --filter @cavelang/query test
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Every §12.1 example pattern and every §12.2 filter runs against a live
|
|
76
|
+
in-memory store, including inverse and transitive-inverse cases,
|
|
77
|
+
current-vs-history semantics and negated patterns.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CAVE-Q → SQL compilation (spec §12).
|
|
3
|
+
*
|
|
4
|
+
* Patterns run over *current beliefs* by default — the latest transaction
|
|
5
|
+
* per claim key (spec §9.1) — pass `all` to match the full history.
|
|
6
|
+
* Inverse verbs compile to the same physical query as their primary
|
|
7
|
+
* (spec §12.1): `?x PART-OF monorepo` and `monorepo CONTAINS ?x` produce
|
|
8
|
+
* identical SQL against canonical rows, with the pattern's subject binding
|
|
9
|
+
* on the object side.
|
|
10
|
+
*
|
|
11
|
+
* Transitive patterns (`terrier EXTENDS+ animal`) compile to a recursive
|
|
12
|
+
* CTE over current, positive, non-retracted edges (depth-capped at 32).
|
|
13
|
+
*/
|
|
14
|
+
import type { Row, Store } from '@cavelang/store';
|
|
15
|
+
/** One query solution: variable bindings plus the matched row (absent for transitive hops). */
|
|
16
|
+
export type Match = {
|
|
17
|
+
readonly bindings: Readonly<Record<string, string>>;
|
|
18
|
+
readonly row?: Row.t;
|
|
19
|
+
};
|
|
20
|
+
export type Options = {
|
|
21
|
+
/** Match all appended rows, not only current beliefs. */
|
|
22
|
+
readonly all?: boolean;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Runs a CAVE-Q query against a store.
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* query(store, '?x USES jwt')
|
|
29
|
+
* query(store, '?cause CAUSE app/crash\n WHERE conf >= 0.7')
|
|
30
|
+
* query(store, 'terrier EXTENDS+ animal')
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare const query: (store: Store, input: string, options?: Options) => Match[];
|
|
34
|
+
//# sourceMappingURL=compile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/compile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AAGjD,+FAA+F;AAC/F,MAAM,MAAM,KAAK,GAAG;IAClB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD,QAAQ,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,OAAO,GAAG;IACpB,yDAAyD;IACzD,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CACvB,CAAA;AAyQD;;;;;;;;GAQG;AACH,eAAO,MAAM,KAAK,GAAI,OAAO,KAAK,EAAE,OAAO,MAAM,EAAE,UAAS,OAAY,KAAG,KAAK,EAS/E,CAAA"}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CAVE-Q → SQL compilation (spec §12).
|
|
3
|
+
*
|
|
4
|
+
* Patterns run over *current beliefs* by default — the latest transaction
|
|
5
|
+
* per claim key (spec §9.1) — pass `all` to match the full history.
|
|
6
|
+
* Inverse verbs compile to the same physical query as their primary
|
|
7
|
+
* (spec §12.1): `?x PART-OF monorepo` and `monorepo CONTAINS ?x` produce
|
|
8
|
+
* identical SQL against canonical rows, with the pattern's subject binding
|
|
9
|
+
* on the object side.
|
|
10
|
+
*
|
|
11
|
+
* Transitive patterns (`terrier EXTENDS+ animal`) compile to a recursive
|
|
12
|
+
* CTE over current, positive, non-retracted edges (depth-capped at 32).
|
|
13
|
+
*/
|
|
14
|
+
import { Uuidv7, Value } from '@cavelang/core';
|
|
15
|
+
import { Registry } from '@cavelang/canonical';
|
|
16
|
+
import * as Pattern from "./pattern.js";
|
|
17
|
+
const currentSql = `
|
|
18
|
+
SELECT c.* FROM cave_claim c
|
|
19
|
+
JOIN (
|
|
20
|
+
SELECT claim_key, MAX(tx) AS max_tx
|
|
21
|
+
FROM cave_claim GROUP BY claim_key
|
|
22
|
+
) latest ON c.claim_key = latest.claim_key AND c.tx = latest.max_tx
|
|
23
|
+
`;
|
|
24
|
+
/**
|
|
25
|
+
* UUIDv7 interval `[lo, hi)` for a tx filter value: a bare date covers the
|
|
26
|
+
* whole UTC day, a timestamp covers one second. Interval semantics keep
|
|
27
|
+
* adjacent operators distinguishable (`<=` includes the boundary day that
|
|
28
|
+
* `<` excludes) and make `WHERE tx = 2026-01-01` mean "recorded that day".
|
|
29
|
+
*/
|
|
30
|
+
const txBounds = (text) => {
|
|
31
|
+
const hasTime = text.includes('T');
|
|
32
|
+
const start = Date.parse(hasTime ? text : `${text}T00:00:00Z`);
|
|
33
|
+
if (Number.isNaN(start)) {
|
|
34
|
+
throw new Error(`CAVE-Q: cannot parse tx date ${JSON.stringify(text)}`);
|
|
35
|
+
}
|
|
36
|
+
const end = start + (hasTime ? 1_000 : 86_400_000);
|
|
37
|
+
return { lo: Uuidv7.at(start, 0, new Uint8Array(8)), hi: Uuidv7.at(end, 0, new Uint8Array(8)) };
|
|
38
|
+
};
|
|
39
|
+
const compile = (pattern, registry, options) => {
|
|
40
|
+
// Inverse resolution (spec §12.1): swap the pattern's endpoint slots and
|
|
41
|
+
// query the primary verb.
|
|
42
|
+
let verb = pattern.verb;
|
|
43
|
+
let subjectSlot = pattern.subject;
|
|
44
|
+
let objectSlot = pattern.payload.kind === 'object' ? pattern.payload.object : undefined;
|
|
45
|
+
if (verb.kind === 'verb') {
|
|
46
|
+
const { primary, isInverse } = Registry.primaryOf(registry, verb.name);
|
|
47
|
+
if (isInverse) {
|
|
48
|
+
if (pattern.payload.kind === 'attribute') {
|
|
49
|
+
throw new Error(`CAVE-Q: inverse verb ${verb.name} cannot take an attribute pattern`);
|
|
50
|
+
}
|
|
51
|
+
const swapped = objectSlot ?? { kind: 'wildcard' };
|
|
52
|
+
objectSlot = subjectSlot;
|
|
53
|
+
subjectSlot = swapped;
|
|
54
|
+
verb = { kind: 'verb', name: primary, transitive: verb.transitive };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (verb.kind === 'verb' && verb.transitive) {
|
|
58
|
+
return compileTransitive(pattern, verb.name, subjectSlot, objectSlot, options);
|
|
59
|
+
}
|
|
60
|
+
const conditions = [`c.negated = ${pattern.negated ? 1 : 0}`];
|
|
61
|
+
const params = [];
|
|
62
|
+
/** var name → row columns it binds; repeated vars add join conditions. */
|
|
63
|
+
const varColumns = new Map();
|
|
64
|
+
const slot = (value, column, requireNotNull) => {
|
|
65
|
+
switch (value.kind) {
|
|
66
|
+
case 'term':
|
|
67
|
+
// A date/number term in object position must also match metric
|
|
68
|
+
// rows, which store their value in value_text with object NULL
|
|
69
|
+
// (`latency IS 30ms` ⇒ pattern `latency IS 30ms` matches).
|
|
70
|
+
if (column === 'object') {
|
|
71
|
+
const parsed = Value.parse(value.text);
|
|
72
|
+
if (parsed.kind === 'number' || parsed.kind === 'date') {
|
|
73
|
+
conditions.push('(c.object = ? OR (c.object IS NULL AND c.value_text = ?))');
|
|
74
|
+
params.push(value.text, value.text);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
conditions.push(`c.${column} = ?`);
|
|
79
|
+
params.push(value.text);
|
|
80
|
+
return;
|
|
81
|
+
case 'var': {
|
|
82
|
+
const columns = varColumns.get(value.name) ?? [];
|
|
83
|
+
columns.push(column);
|
|
84
|
+
varColumns.set(value.name, columns);
|
|
85
|
+
if (requireNotNull) {
|
|
86
|
+
conditions.push(`c.${column} IS NOT NULL`);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
case 'wildcard':
|
|
91
|
+
if (requireNotNull) {
|
|
92
|
+
conditions.push(`c.${column} IS NOT NULL`);
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
slot(subjectSlot, 'subject', false);
|
|
98
|
+
if (verb.kind === 'verb') {
|
|
99
|
+
conditions.push('c.verb = ?');
|
|
100
|
+
params.push(verb.name);
|
|
101
|
+
}
|
|
102
|
+
else if (verb.kind === 'var') {
|
|
103
|
+
const columns = varColumns.get(verb.name) ?? [];
|
|
104
|
+
columns.push('verb');
|
|
105
|
+
varColumns.set(verb.name, columns);
|
|
106
|
+
}
|
|
107
|
+
if (objectSlot !== undefined) {
|
|
108
|
+
slot(objectSlot, 'object', true);
|
|
109
|
+
}
|
|
110
|
+
if (pattern.payload.kind === 'attribute') {
|
|
111
|
+
conditions.push('c.attribute = ?');
|
|
112
|
+
params.push(pattern.payload.attribute);
|
|
113
|
+
slot(pattern.payload.value, 'value_text', true);
|
|
114
|
+
}
|
|
115
|
+
for (const columns of varColumns.values()) {
|
|
116
|
+
for (let i = 1; i < columns.length; i++) {
|
|
117
|
+
conditions.push(`c.${columns[0]} = c.${columns[i]}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
for (const context of pattern.contexts) {
|
|
121
|
+
conditions.push('EXISTS (SELECT 1 FROM cave_context x WHERE x.claim_id = c.id AND x.context = ?)');
|
|
122
|
+
params.push(context);
|
|
123
|
+
}
|
|
124
|
+
for (const tag of pattern.tags) {
|
|
125
|
+
if (tag.value === undefined) {
|
|
126
|
+
conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ? AND t.value IS NULL)');
|
|
127
|
+
params.push(tag.key);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ? AND t.value = ?)');
|
|
131
|
+
params.push(tag.key, tag.value);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const filter of pattern.filters) {
|
|
135
|
+
switch (filter.field) {
|
|
136
|
+
case 'conf':
|
|
137
|
+
conditions.push(`c.conf ${filter.op} ?`);
|
|
138
|
+
params.push(filter.value);
|
|
139
|
+
break;
|
|
140
|
+
case 'tag':
|
|
141
|
+
if (filter.value === undefined) {
|
|
142
|
+
conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ?)');
|
|
143
|
+
params.push(filter.key);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ? AND t.value = ?)');
|
|
147
|
+
params.push(filter.key, filter.value);
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
case 'context':
|
|
151
|
+
conditions.push('EXISTS (SELECT 1 FROM cave_context x WHERE x.claim_id = c.id AND x.context = ?)');
|
|
152
|
+
params.push(filter.value);
|
|
153
|
+
break;
|
|
154
|
+
case 'value':
|
|
155
|
+
conditions.push(`c.value_num ${filter.op} ?`);
|
|
156
|
+
params.push(filter.value);
|
|
157
|
+
if (filter.unit !== undefined) {
|
|
158
|
+
conditions.push('c.value_unit = ?');
|
|
159
|
+
params.push(filter.unit);
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
case 'tx': {
|
|
163
|
+
const { lo, hi } = txBounds(filter.value);
|
|
164
|
+
switch (filter.op) {
|
|
165
|
+
case '>':
|
|
166
|
+
conditions.push('c.tx >= ?');
|
|
167
|
+
params.push(hi);
|
|
168
|
+
break;
|
|
169
|
+
case '>=':
|
|
170
|
+
conditions.push('c.tx >= ?');
|
|
171
|
+
params.push(lo);
|
|
172
|
+
break;
|
|
173
|
+
case '<':
|
|
174
|
+
conditions.push('c.tx < ?');
|
|
175
|
+
params.push(lo);
|
|
176
|
+
break;
|
|
177
|
+
case '<=':
|
|
178
|
+
conditions.push('c.tx < ?');
|
|
179
|
+
params.push(hi);
|
|
180
|
+
break;
|
|
181
|
+
case '=':
|
|
182
|
+
conditions.push('(c.tx >= ? AND c.tx < ?)');
|
|
183
|
+
params.push(lo, hi);
|
|
184
|
+
break;
|
|
185
|
+
case '!=':
|
|
186
|
+
conditions.push('(c.tx < ? OR c.tx >= ?)');
|
|
187
|
+
params.push(lo, hi);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Positive patterns match supported beliefs: a retracted (@ 0%) current
|
|
195
|
+
// belief has no current support (§9.3) and is skipped — mirroring the
|
|
196
|
+
// transitive CTE and store traversal — unless the query asks about
|
|
197
|
+
// confidence explicitly or runs over the full history.
|
|
198
|
+
if (options.all !== true && !pattern.filters.some(filter => filter.field === 'conf')) {
|
|
199
|
+
conditions.push('c.conf > 0');
|
|
200
|
+
}
|
|
201
|
+
const base = options.all === true ? 'SELECT * FROM cave_claim' : currentSql;
|
|
202
|
+
const sql = `SELECT c.* FROM (${base}) c WHERE ${conditions.join(' AND ')} ORDER BY c.tx`;
|
|
203
|
+
const bind = (row) => {
|
|
204
|
+
const bindings = {};
|
|
205
|
+
for (const [name, columns] of varColumns) {
|
|
206
|
+
bindings[name] = String(row[columns[0]]);
|
|
207
|
+
}
|
|
208
|
+
return bindings;
|
|
209
|
+
};
|
|
210
|
+
return { sql, params, bind, transitive: false };
|
|
211
|
+
};
|
|
212
|
+
const compileTransitive = (pattern, verb, subjectSlot, objectSlot, options) => {
|
|
213
|
+
if (pattern.negated || pattern.filters.length > 0 || pattern.contexts.length > 0 || pattern.tags.length > 0 ||
|
|
214
|
+
pattern.payload.kind === 'attribute') {
|
|
215
|
+
throw new Error('CAVE-Q: transitive patterns support subject/object slots only (spec §12.1)');
|
|
216
|
+
}
|
|
217
|
+
const base = options.all === true ? 'SELECT * FROM cave_claim' : currentSql;
|
|
218
|
+
const conditions = [];
|
|
219
|
+
const params = [verb];
|
|
220
|
+
if (subjectSlot.kind === 'term') {
|
|
221
|
+
conditions.push('h.src = ?');
|
|
222
|
+
params.push(subjectSlot.text);
|
|
223
|
+
}
|
|
224
|
+
if (objectSlot?.kind === 'term') {
|
|
225
|
+
conditions.push('h.dst = ?');
|
|
226
|
+
params.push(objectSlot.text);
|
|
227
|
+
}
|
|
228
|
+
// A repeated variable forces equality here just as in single-hop
|
|
229
|
+
// patterns: `?x EXTENDS+ ?x` asks for nodes on a cycle, not for every
|
|
230
|
+
// reachable pair.
|
|
231
|
+
if (subjectSlot.kind === 'var' && objectSlot?.kind === 'var' && subjectSlot.name === objectSlot.name) {
|
|
232
|
+
conditions.push('h.src = h.dst');
|
|
233
|
+
}
|
|
234
|
+
const sql = `
|
|
235
|
+
WITH RECURSIVE cur AS (
|
|
236
|
+
SELECT c.subject AS src, c.object AS dst
|
|
237
|
+
FROM (${base}) c
|
|
238
|
+
WHERE c.verb = ? AND c.negated = 0 AND c.conf > 0 AND c.object IS NOT NULL
|
|
239
|
+
), hops(src, dst, depth) AS (
|
|
240
|
+
SELECT src, dst, 1 FROM cur
|
|
241
|
+
UNION
|
|
242
|
+
SELECT h.src, cur.dst, h.depth + 1 FROM hops h JOIN cur ON cur.src = h.dst
|
|
243
|
+
WHERE h.depth < 32
|
|
244
|
+
)
|
|
245
|
+
SELECT DISTINCT h.src AS src, h.dst AS dst FROM hops h
|
|
246
|
+
${conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''}
|
|
247
|
+
ORDER BY h.src, h.dst`;
|
|
248
|
+
const bind = (row) => {
|
|
249
|
+
const bindings = {};
|
|
250
|
+
if (subjectSlot.kind === 'var') {
|
|
251
|
+
bindings[subjectSlot.name] = String(row['src']);
|
|
252
|
+
}
|
|
253
|
+
if (objectSlot?.kind === 'var') {
|
|
254
|
+
bindings[objectSlot.name] = String(row['dst']);
|
|
255
|
+
}
|
|
256
|
+
return bindings;
|
|
257
|
+
};
|
|
258
|
+
return { sql, params, bind, transitive: true };
|
|
259
|
+
};
|
|
260
|
+
/**
|
|
261
|
+
* Runs a CAVE-Q query against a store.
|
|
262
|
+
*
|
|
263
|
+
* ```ts
|
|
264
|
+
* query(store, '?x USES jwt')
|
|
265
|
+
* query(store, '?cause CAUSE app/crash\n WHERE conf >= 0.7')
|
|
266
|
+
* query(store, 'terrier EXTENDS+ animal')
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
export const query = (store, input, options = {}) => {
|
|
270
|
+
const pattern = Pattern.parse(input);
|
|
271
|
+
const compiled = compile(pattern, store.registry(), options);
|
|
272
|
+
const rows = store.db.prepare(compiled.sql).all(...compiled.params);
|
|
273
|
+
return rows.map(row => compiled.transitive ?
|
|
274
|
+
{ bindings: compiled.bind(row) } :
|
|
275
|
+
{ bindings: compiled.bind(row), row: row });
|
|
276
|
+
};
|
|
277
|
+
//# sourceMappingURL=compile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.js","sourceRoot":"","sources":["../../src/compile.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cavelang/query` — CAVE-Q, the graph-pattern query layer (spec §12).
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { query } from '@cavelang/query'
|
|
6
|
+
*
|
|
7
|
+
* query(store, '?x USES jwt') // [{ bindings: { x: 'auth/middleware' }, row }]
|
|
8
|
+
* query(store, '?x HAS bug: ?bug #security')
|
|
9
|
+
* query(store, '?x PART-OF monorepo') // inverse verbs compile to canonical rows
|
|
10
|
+
* query(store, 'terrier EXTENDS+ animal') // transitive
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export * as Pattern from './pattern.ts';
|
|
14
|
+
export { query } from './compile.ts';
|
|
15
|
+
export type { Match, Options } from './compile.ts';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AACpC,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cavelang/query` — CAVE-Q, the graph-pattern query layer (spec §12).
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { query } from '@cavelang/query'
|
|
6
|
+
*
|
|
7
|
+
* query(store, '?x USES jwt') // [{ bindings: { x: 'auth/middleware' }, row }]
|
|
8
|
+
* query(store, '?x HAS bug: ?bug #security')
|
|
9
|
+
* query(store, '?x PART-OF monorepo') // inverse verbs compile to canonical rows
|
|
10
|
+
* query(store, 'terrier EXTENDS+ animal') // transitive
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export * as Pattern from "./pattern.js";
|
|
14
|
+
export { query } from "./compile.js";
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CAVE-Q patterns (spec §12.1).
|
|
3
|
+
*
|
|
4
|
+
* ```cave
|
|
5
|
+
* ?x USES jwt ; all systems using jwt
|
|
6
|
+
* ?x HAS bug: ?bug #security ; all security bugs
|
|
7
|
+
* ?cause CAUSE app/crash ; candidate causes
|
|
8
|
+
* WHERE conf >= 0.7
|
|
9
|
+
* ?x ?verb ?y @production ; all production facts
|
|
10
|
+
* terrier EXTENDS+ animal ; transitive hops
|
|
11
|
+
* _ USES jwt ; wildcard
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* A query is one pattern line followed by any number of `WHERE` filter
|
|
15
|
+
* lines (spec §12.2), indented or not.
|
|
16
|
+
*/
|
|
17
|
+
/** A pattern slot: named variable, wildcard `_`, or a bound term. */
|
|
18
|
+
export type Slot = {
|
|
19
|
+
readonly kind: 'var';
|
|
20
|
+
readonly name: string;
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: 'wildcard';
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: 'term';
|
|
25
|
+
readonly text: string;
|
|
26
|
+
};
|
|
27
|
+
/** Verb position: concrete verb (possibly transitive `VERB+`), variable, or wildcard. */
|
|
28
|
+
export type VerbSlot = {
|
|
29
|
+
readonly kind: 'verb';
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly transitive: boolean;
|
|
32
|
+
} | {
|
|
33
|
+
readonly kind: 'var';
|
|
34
|
+
readonly name: string;
|
|
35
|
+
} | {
|
|
36
|
+
readonly kind: 'wildcard';
|
|
37
|
+
};
|
|
38
|
+
export type PayloadPattern = {
|
|
39
|
+
readonly kind: 'object';
|
|
40
|
+
readonly object: Slot;
|
|
41
|
+
} | {
|
|
42
|
+
readonly kind: 'attribute';
|
|
43
|
+
readonly attribute: string;
|
|
44
|
+
readonly value: Slot;
|
|
45
|
+
} | {
|
|
46
|
+
readonly kind: 'any';
|
|
47
|
+
};
|
|
48
|
+
export type FilterOp = '=' | '!=' | '>' | '>=' | '<' | '<=';
|
|
49
|
+
export type Filter = {
|
|
50
|
+
readonly field: 'conf';
|
|
51
|
+
readonly op: FilterOp;
|
|
52
|
+
readonly value: number;
|
|
53
|
+
} | {
|
|
54
|
+
readonly field: 'tag';
|
|
55
|
+
readonly op: '=';
|
|
56
|
+
readonly key: string;
|
|
57
|
+
readonly value?: string;
|
|
58
|
+
} | {
|
|
59
|
+
readonly field: 'context';
|
|
60
|
+
readonly op: '=';
|
|
61
|
+
readonly value: string;
|
|
62
|
+
} | {
|
|
63
|
+
readonly field: 'value';
|
|
64
|
+
readonly op: FilterOp;
|
|
65
|
+
readonly value: number;
|
|
66
|
+
readonly unit?: string;
|
|
67
|
+
} | {
|
|
68
|
+
readonly field: 'tx';
|
|
69
|
+
readonly op: FilterOp;
|
|
70
|
+
readonly value: string;
|
|
71
|
+
};
|
|
72
|
+
export type Pattern = {
|
|
73
|
+
readonly subject: Slot;
|
|
74
|
+
readonly verb: VerbSlot;
|
|
75
|
+
readonly payload: PayloadPattern;
|
|
76
|
+
readonly negated: boolean;
|
|
77
|
+
/** `@ctx` filters on the pattern line. */
|
|
78
|
+
readonly contexts: readonly string[];
|
|
79
|
+
/** `#tag[:value]` filters on the pattern line. */
|
|
80
|
+
readonly tags: readonly {
|
|
81
|
+
key: string;
|
|
82
|
+
value?: string;
|
|
83
|
+
}[];
|
|
84
|
+
readonly filters: readonly Filter[];
|
|
85
|
+
};
|
|
86
|
+
export type t = Pattern;
|
|
87
|
+
/** Parses a CAVE-Q query: one pattern line plus `WHERE` filter lines. */
|
|
88
|
+
export declare const parse: (input: string) => Pattern;
|
|
89
|
+
//# sourceMappingURL=pattern.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pattern.d.ts","sourceRoot":"","sources":["../../src/pattern.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CAVE-Q patterns (spec §12.1).
|
|
3
|
+
*
|
|
4
|
+
* ```cave
|
|
5
|
+
* ?x USES jwt ; all systems using jwt
|
|
6
|
+
* ?x HAS bug: ?bug #security ; all security bugs
|
|
7
|
+
* ?cause CAUSE app/crash ; candidate causes
|
|
8
|
+
* WHERE conf >= 0.7
|
|
9
|
+
* ?x ?verb ?y @production ; all production facts
|
|
10
|
+
* terrier EXTENDS+ animal ; transitive hops
|
|
11
|
+
* _ USES jwt ; wildcard
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* A query is one pattern line followed by any number of `WHERE` filter
|
|
15
|
+
* lines (spec §12.2), indented or not.
|
|
16
|
+
*/
|
|
17
|
+
import { Confidence, Verb } from '@cavelang/core';
|
|
18
|
+
import { Token } from '@cavelang/parser';
|
|
19
|
+
const filterOps = ['>=', '<=', '!=', '=', '>', '<'];
|
|
20
|
+
const isFilterOp = (text) => filterOps.includes(text);
|
|
21
|
+
const slotOf = (token) => {
|
|
22
|
+
if (token.kind === 'word') {
|
|
23
|
+
if (token.text === '_') {
|
|
24
|
+
return { kind: 'wildcard' };
|
|
25
|
+
}
|
|
26
|
+
if (token.text.startsWith('?')) {
|
|
27
|
+
return { kind: 'var', name: token.text.slice(1) };
|
|
28
|
+
}
|
|
29
|
+
return { kind: 'term', text: token.text };
|
|
30
|
+
}
|
|
31
|
+
const delimiter = token.kind === 'code' ? '`' : '"';
|
|
32
|
+
return { kind: 'term', text: `${delimiter}${token.text}${delimiter}` };
|
|
33
|
+
};
|
|
34
|
+
const verbSlotOf = (token) => {
|
|
35
|
+
if (token.kind !== 'word') {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
if (token.text === '_') {
|
|
39
|
+
return { kind: 'wildcard' };
|
|
40
|
+
}
|
|
41
|
+
if (token.text.startsWith('?')) {
|
|
42
|
+
return { kind: 'var', name: token.text.slice(1) };
|
|
43
|
+
}
|
|
44
|
+
const transitive = token.text.endsWith('+');
|
|
45
|
+
const name = transitive ? token.text.slice(0, -1) : token.text;
|
|
46
|
+
return Verb.isVerbToken(name) ? { kind: 'verb', name, transitive } : undefined;
|
|
47
|
+
};
|
|
48
|
+
const parseFilter = (tokens, lineNo) => {
|
|
49
|
+
const [field, op, ...valueTokens] = tokens;
|
|
50
|
+
const bad = (message) => {
|
|
51
|
+
throw new Error(`CAVE-Q line ${lineNo}: ${message}`);
|
|
52
|
+
};
|
|
53
|
+
if (field?.kind !== 'word' || op?.kind !== 'word' || valueTokens.length === 0) {
|
|
54
|
+
return bad('expected "WHERE <field> <op> <value>" (spec §12.2)');
|
|
55
|
+
}
|
|
56
|
+
if (!isFilterOp(op.text)) {
|
|
57
|
+
return bad(`unknown operator ${JSON.stringify(op.text)}`);
|
|
58
|
+
}
|
|
59
|
+
const valueText = valueTokens.map(token => token.text).join(' ');
|
|
60
|
+
switch (field.text) {
|
|
61
|
+
case 'conf': {
|
|
62
|
+
const conf = valueText.endsWith('%') ? Confidence.parse(valueText) : Number(valueText);
|
|
63
|
+
if (conf === undefined || Number.isNaN(conf)) {
|
|
64
|
+
return bad(`cannot parse confidence ${JSON.stringify(valueText)}`);
|
|
65
|
+
}
|
|
66
|
+
return { field: 'conf', op: op.text, value: conf };
|
|
67
|
+
}
|
|
68
|
+
case 'tag': {
|
|
69
|
+
if (op.text !== '=') {
|
|
70
|
+
return bad('tag filters support = only');
|
|
71
|
+
}
|
|
72
|
+
const colonAt = valueText.indexOf(':');
|
|
73
|
+
return colonAt === -1 ?
|
|
74
|
+
{ field: 'tag', op: '=', key: valueText } :
|
|
75
|
+
{ field: 'tag', op: '=', key: valueText.slice(0, colonAt), value: valueText.slice(colonAt + 1) };
|
|
76
|
+
}
|
|
77
|
+
case 'context': {
|
|
78
|
+
if (op.text !== '=') {
|
|
79
|
+
return bad('context filters support = only');
|
|
80
|
+
}
|
|
81
|
+
return { field: 'context', op: '=', value: valueText };
|
|
82
|
+
}
|
|
83
|
+
case 'value': {
|
|
84
|
+
const match = /^(-?\d+(?:\.\d+)?)(?:\s+(.+))?$/.exec(valueText);
|
|
85
|
+
if (!match) {
|
|
86
|
+
return bad(`cannot parse value filter ${JSON.stringify(valueText)}`);
|
|
87
|
+
}
|
|
88
|
+
const unit = match[2];
|
|
89
|
+
return { field: 'value', op: op.text, value: Number(match[1]), ...unit === undefined ? {} : { unit } };
|
|
90
|
+
}
|
|
91
|
+
case 'tx':
|
|
92
|
+
return { field: 'tx', op: op.text, value: valueText };
|
|
93
|
+
default:
|
|
94
|
+
return bad(`unknown filter field ${JSON.stringify(field.text)}`);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/** Parses a CAVE-Q query: one pattern line plus `WHERE` filter lines. */
|
|
98
|
+
export const parse = (input) => {
|
|
99
|
+
const lines = input
|
|
100
|
+
.split(/\r?\n/)
|
|
101
|
+
.map(line => line.trim())
|
|
102
|
+
.filter(line => line !== '' && !line.startsWith(';'));
|
|
103
|
+
if (lines.length === 0) {
|
|
104
|
+
throw new Error('CAVE-Q: empty query');
|
|
105
|
+
}
|
|
106
|
+
const [patternLine, ...filterLines] = lines;
|
|
107
|
+
const { head } = Token.splitComment(patternLine);
|
|
108
|
+
const tokens = Token.tokenize(head);
|
|
109
|
+
if (tokens.length < 2) {
|
|
110
|
+
throw new Error('CAVE-Q: a pattern needs at least a subject and a verb (spec §12.1)');
|
|
111
|
+
}
|
|
112
|
+
const subject = slotOf(tokens[0]);
|
|
113
|
+
const verb = verbSlotOf(tokens[1]);
|
|
114
|
+
if (verb === undefined) {
|
|
115
|
+
throw new Error(`CAVE-Q: cannot parse verb position ${JSON.stringify(tokens[1].text)}`);
|
|
116
|
+
}
|
|
117
|
+
let rest = tokens.slice(2);
|
|
118
|
+
const negated = rest[0]?.kind === 'word' && rest[0].text === 'NOT';
|
|
119
|
+
if (negated) {
|
|
120
|
+
rest = rest.slice(1);
|
|
121
|
+
}
|
|
122
|
+
const contexts = [];
|
|
123
|
+
const tags = [];
|
|
124
|
+
const payloadTokens = [];
|
|
125
|
+
for (const token of rest) {
|
|
126
|
+
if (token.kind === 'word' && token.text.startsWith('@') && token.text.length > 1) {
|
|
127
|
+
contexts.push(token.text.slice(1));
|
|
128
|
+
}
|
|
129
|
+
else if (token.kind === 'word' && token.text.startsWith('#') && token.text.length > 1) {
|
|
130
|
+
const body = token.text.slice(1);
|
|
131
|
+
const colonAt = body.indexOf(':');
|
|
132
|
+
tags.push(colonAt === -1 ? { key: body } : { key: body.slice(0, colonAt), value: body.slice(colonAt + 1) });
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
payloadTokens.push(token);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
let payload;
|
|
139
|
+
if (payloadTokens.length === 0) {
|
|
140
|
+
payload = { kind: 'any' };
|
|
141
|
+
}
|
|
142
|
+
else if (payloadTokens[0].kind === 'word' && payloadTokens[0].text.endsWith(':') && payloadTokens[0].text.length > 1) {
|
|
143
|
+
if (payloadTokens.length !== 2) {
|
|
144
|
+
throw new Error('CAVE-Q: attribute patterns take exactly one value slot (spec §12.1)');
|
|
145
|
+
}
|
|
146
|
+
payload = { kind: 'attribute', attribute: payloadTokens[0].text.slice(0, -1), value: slotOf(payloadTokens[1]) };
|
|
147
|
+
}
|
|
148
|
+
else if (payloadTokens.length === 1) {
|
|
149
|
+
payload = { kind: 'object', object: slotOf(payloadTokens[0]) };
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
throw new Error(`CAVE-Q: cannot parse payload ${JSON.stringify(payloadTokens.map(token => token.text).join(' '))}`);
|
|
153
|
+
}
|
|
154
|
+
const filters = filterLines.map((line, at) => {
|
|
155
|
+
const filterTokens = Token.tokenize(Token.splitComment(line).head);
|
|
156
|
+
const [keyword, ...rest_] = filterTokens;
|
|
157
|
+
if (keyword?.kind !== 'word' || keyword.text !== 'WHERE') {
|
|
158
|
+
throw new Error(`CAVE-Q line ${at + 2}: expected WHERE, got ${JSON.stringify(keyword?.text ?? '')}`);
|
|
159
|
+
}
|
|
160
|
+
return parseFilter(rest_, at + 2);
|
|
161
|
+
});
|
|
162
|
+
return { subject, verb, payload, negated, contexts, tags, filters };
|
|
163
|
+
};
|
|
164
|
+
//# sourceMappingURL=pattern.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pattern.js","sourceRoot":"","sources":["../../src/pattern.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cavelang/query",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CAVE-Q — graph-pattern queries (?x vars, wildcards, inverse verbs, transitive hops, WHERE filters) compiled to SQL.",
|
|
6
|
+
"license": "CC0-1.0",
|
|
7
|
+
"author": "Mirek Rusin (https://github.com/mirek)",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/mirek/cave.git",
|
|
11
|
+
"directory": "packages/query"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.18"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/src/index.d.ts",
|
|
19
|
+
"default": "./dist/src/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"!dist/test",
|
|
25
|
+
"!dist/tsconfig.tsbuildinfo",
|
|
26
|
+
"src",
|
|
27
|
+
"!src/**/*.test.ts",
|
|
28
|
+
"README.md",
|
|
29
|
+
"License.md"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@cavelang/canonical": "0.1.0",
|
|
36
|
+
"@cavelang/core": "0.1.0",
|
|
37
|
+
"@cavelang/parser": "0.1.0",
|
|
38
|
+
"@cavelang/store": "0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -b",
|
|
42
|
+
"test": "node --disable-warning=ExperimentalWarning --test 'test/*.test.ts'",
|
|
43
|
+
"typecheck": "tsc -b"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/compile.ts
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CAVE-Q → SQL compilation (spec §12).
|
|
3
|
+
*
|
|
4
|
+
* Patterns run over *current beliefs* by default — the latest transaction
|
|
5
|
+
* per claim key (spec §9.1) — pass `all` to match the full history.
|
|
6
|
+
* Inverse verbs compile to the same physical query as their primary
|
|
7
|
+
* (spec §12.1): `?x PART-OF monorepo` and `monorepo CONTAINS ?x` produce
|
|
8
|
+
* identical SQL against canonical rows, with the pattern's subject binding
|
|
9
|
+
* on the object side.
|
|
10
|
+
*
|
|
11
|
+
* Transitive patterns (`terrier EXTENDS+ animal`) compile to a recursive
|
|
12
|
+
* CTE over current, positive, non-retracted edges (depth-capped at 32).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Uuidv7, Value } from '@cavelang/core'
|
|
16
|
+
import { Registry } from '@cavelang/canonical'
|
|
17
|
+
import type { Row, Store } from '@cavelang/store'
|
|
18
|
+
import * as Pattern from './pattern.ts'
|
|
19
|
+
|
|
20
|
+
/** One query solution: variable bindings plus the matched row (absent for transitive hops). */
|
|
21
|
+
export type Match = {
|
|
22
|
+
readonly bindings: Readonly<Record<string, string>>
|
|
23
|
+
readonly row?: Row.t
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type Options = {
|
|
27
|
+
/** Match all appended rows, not only current beliefs. */
|
|
28
|
+
readonly all?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const currentSql = `
|
|
32
|
+
SELECT c.* FROM cave_claim c
|
|
33
|
+
JOIN (
|
|
34
|
+
SELECT claim_key, MAX(tx) AS max_tx
|
|
35
|
+
FROM cave_claim GROUP BY claim_key
|
|
36
|
+
) latest ON c.claim_key = latest.claim_key AND c.tx = latest.max_tx
|
|
37
|
+
`
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* UUIDv7 interval `[lo, hi)` for a tx filter value: a bare date covers the
|
|
41
|
+
* whole UTC day, a timestamp covers one second. Interval semantics keep
|
|
42
|
+
* adjacent operators distinguishable (`<=` includes the boundary day that
|
|
43
|
+
* `<` excludes) and make `WHERE tx = 2026-01-01` mean "recorded that day".
|
|
44
|
+
*/
|
|
45
|
+
const txBounds = (text: string): { lo: string, hi: string } => {
|
|
46
|
+
const hasTime = text.includes('T')
|
|
47
|
+
const start = Date.parse(hasTime ? text : `${text}T00:00:00Z`)
|
|
48
|
+
if (Number.isNaN(start)) {
|
|
49
|
+
throw new Error(`CAVE-Q: cannot parse tx date ${JSON.stringify(text)}`)
|
|
50
|
+
}
|
|
51
|
+
const end = start + (hasTime ? 1_000 : 86_400_000)
|
|
52
|
+
return { lo: Uuidv7.at(start, 0, new Uint8Array(8)), hi: Uuidv7.at(end, 0, new Uint8Array(8)) }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type Compiled = {
|
|
56
|
+
readonly sql: string
|
|
57
|
+
readonly params: (string | number)[]
|
|
58
|
+
readonly bind: (row: Record<string, unknown>) => Record<string, string>
|
|
59
|
+
readonly transitive: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const compile = (pattern: Pattern.t, registry: Registry.t, options: Options): Compiled => {
|
|
63
|
+
// Inverse resolution (spec §12.1): swap the pattern's endpoint slots and
|
|
64
|
+
// query the primary verb.
|
|
65
|
+
let verb = pattern.verb
|
|
66
|
+
let subjectSlot = pattern.subject
|
|
67
|
+
let objectSlot: undefined | Pattern.Slot =
|
|
68
|
+
pattern.payload.kind === 'object' ? pattern.payload.object : undefined
|
|
69
|
+
if (verb.kind === 'verb') {
|
|
70
|
+
const { primary, isInverse } = Registry.primaryOf(registry, verb.name)
|
|
71
|
+
if (isInverse) {
|
|
72
|
+
if (pattern.payload.kind === 'attribute') {
|
|
73
|
+
throw new Error(`CAVE-Q: inverse verb ${verb.name} cannot take an attribute pattern`)
|
|
74
|
+
}
|
|
75
|
+
const swapped = objectSlot ?? { kind: 'wildcard' as const }
|
|
76
|
+
objectSlot = subjectSlot
|
|
77
|
+
subjectSlot = swapped
|
|
78
|
+
verb = { kind: 'verb', name: primary, transitive: verb.transitive }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (verb.kind === 'verb' && verb.transitive) {
|
|
83
|
+
return compileTransitive(pattern, verb.name, subjectSlot, objectSlot, options)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const conditions: string[] = [`c.negated = ${pattern.negated ? 1 : 0}`]
|
|
87
|
+
const params: (string | number)[] = []
|
|
88
|
+
/** var name → row columns it binds; repeated vars add join conditions. */
|
|
89
|
+
const varColumns = new Map<string, string[]>()
|
|
90
|
+
const slot = (value: Pattern.Slot, column: string, requireNotNull: boolean): void => {
|
|
91
|
+
switch (value.kind) {
|
|
92
|
+
case 'term':
|
|
93
|
+
// A date/number term in object position must also match metric
|
|
94
|
+
// rows, which store their value in value_text with object NULL
|
|
95
|
+
// (`latency IS 30ms` ⇒ pattern `latency IS 30ms` matches).
|
|
96
|
+
if (column === 'object') {
|
|
97
|
+
const parsed = Value.parse(value.text)
|
|
98
|
+
if (parsed.kind === 'number' || parsed.kind === 'date') {
|
|
99
|
+
conditions.push('(c.object = ? OR (c.object IS NULL AND c.value_text = ?))')
|
|
100
|
+
params.push(value.text, value.text)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
conditions.push(`c.${column} = ?`)
|
|
105
|
+
params.push(value.text)
|
|
106
|
+
return
|
|
107
|
+
case 'var': {
|
|
108
|
+
const columns = varColumns.get(value.name) ?? []
|
|
109
|
+
columns.push(column)
|
|
110
|
+
varColumns.set(value.name, columns)
|
|
111
|
+
if (requireNotNull) {
|
|
112
|
+
conditions.push(`c.${column} IS NOT NULL`)
|
|
113
|
+
}
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
case 'wildcard':
|
|
117
|
+
if (requireNotNull) {
|
|
118
|
+
conditions.push(`c.${column} IS NOT NULL`)
|
|
119
|
+
}
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
slot(subjectSlot, 'subject', false)
|
|
125
|
+
if (verb.kind === 'verb') {
|
|
126
|
+
conditions.push('c.verb = ?')
|
|
127
|
+
params.push(verb.name)
|
|
128
|
+
} else if (verb.kind === 'var') {
|
|
129
|
+
const columns = varColumns.get(verb.name) ?? []
|
|
130
|
+
columns.push('verb')
|
|
131
|
+
varColumns.set(verb.name, columns)
|
|
132
|
+
}
|
|
133
|
+
if (objectSlot !== undefined) {
|
|
134
|
+
slot(objectSlot, 'object', true)
|
|
135
|
+
}
|
|
136
|
+
if (pattern.payload.kind === 'attribute') {
|
|
137
|
+
conditions.push('c.attribute = ?')
|
|
138
|
+
params.push(pattern.payload.attribute)
|
|
139
|
+
slot(pattern.payload.value, 'value_text', true)
|
|
140
|
+
}
|
|
141
|
+
for (const columns of varColumns.values()) {
|
|
142
|
+
for (let i = 1; i < columns.length; i++) {
|
|
143
|
+
conditions.push(`c.${columns[0]} = c.${columns[i]}`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const context of pattern.contexts) {
|
|
147
|
+
conditions.push('EXISTS (SELECT 1 FROM cave_context x WHERE x.claim_id = c.id AND x.context = ?)')
|
|
148
|
+
params.push(context)
|
|
149
|
+
}
|
|
150
|
+
for (const tag of pattern.tags) {
|
|
151
|
+
if (tag.value === undefined) {
|
|
152
|
+
conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ? AND t.value IS NULL)')
|
|
153
|
+
params.push(tag.key)
|
|
154
|
+
} else {
|
|
155
|
+
conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ? AND t.value = ?)')
|
|
156
|
+
params.push(tag.key, tag.value)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const filter of pattern.filters) {
|
|
160
|
+
switch (filter.field) {
|
|
161
|
+
case 'conf':
|
|
162
|
+
conditions.push(`c.conf ${filter.op} ?`)
|
|
163
|
+
params.push(filter.value)
|
|
164
|
+
break
|
|
165
|
+
case 'tag':
|
|
166
|
+
if (filter.value === undefined) {
|
|
167
|
+
conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ?)')
|
|
168
|
+
params.push(filter.key)
|
|
169
|
+
} else {
|
|
170
|
+
conditions.push('EXISTS (SELECT 1 FROM cave_tag t WHERE t.claim_id = c.id AND t.key = ? AND t.value = ?)')
|
|
171
|
+
params.push(filter.key, filter.value)
|
|
172
|
+
}
|
|
173
|
+
break
|
|
174
|
+
case 'context':
|
|
175
|
+
conditions.push('EXISTS (SELECT 1 FROM cave_context x WHERE x.claim_id = c.id AND x.context = ?)')
|
|
176
|
+
params.push(filter.value)
|
|
177
|
+
break
|
|
178
|
+
case 'value':
|
|
179
|
+
conditions.push(`c.value_num ${filter.op} ?`)
|
|
180
|
+
params.push(filter.value)
|
|
181
|
+
if (filter.unit !== undefined) {
|
|
182
|
+
conditions.push('c.value_unit = ?')
|
|
183
|
+
params.push(filter.unit)
|
|
184
|
+
}
|
|
185
|
+
break
|
|
186
|
+
case 'tx': {
|
|
187
|
+
const { lo, hi } = txBounds(filter.value)
|
|
188
|
+
switch (filter.op) {
|
|
189
|
+
case '>':
|
|
190
|
+
conditions.push('c.tx >= ?')
|
|
191
|
+
params.push(hi)
|
|
192
|
+
break
|
|
193
|
+
case '>=':
|
|
194
|
+
conditions.push('c.tx >= ?')
|
|
195
|
+
params.push(lo)
|
|
196
|
+
break
|
|
197
|
+
case '<':
|
|
198
|
+
conditions.push('c.tx < ?')
|
|
199
|
+
params.push(lo)
|
|
200
|
+
break
|
|
201
|
+
case '<=':
|
|
202
|
+
conditions.push('c.tx < ?')
|
|
203
|
+
params.push(hi)
|
|
204
|
+
break
|
|
205
|
+
case '=':
|
|
206
|
+
conditions.push('(c.tx >= ? AND c.tx < ?)')
|
|
207
|
+
params.push(lo, hi)
|
|
208
|
+
break
|
|
209
|
+
case '!=':
|
|
210
|
+
conditions.push('(c.tx < ? OR c.tx >= ?)')
|
|
211
|
+
params.push(lo, hi)
|
|
212
|
+
break
|
|
213
|
+
}
|
|
214
|
+
break
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Positive patterns match supported beliefs: a retracted (@ 0%) current
|
|
220
|
+
// belief has no current support (§9.3) and is skipped — mirroring the
|
|
221
|
+
// transitive CTE and store traversal — unless the query asks about
|
|
222
|
+
// confidence explicitly or runs over the full history.
|
|
223
|
+
if (options.all !== true && !pattern.filters.some(filter => filter.field === 'conf')) {
|
|
224
|
+
conditions.push('c.conf > 0')
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const base = options.all === true ? 'SELECT * FROM cave_claim' : currentSql
|
|
228
|
+
const sql = `SELECT c.* FROM (${base}) c WHERE ${conditions.join(' AND ')} ORDER BY c.tx`
|
|
229
|
+
const bind = (row: Record<string, unknown>): Record<string, string> => {
|
|
230
|
+
const bindings: Record<string, string> = {}
|
|
231
|
+
for (const [name, columns] of varColumns) {
|
|
232
|
+
bindings[name] = String(row[columns[0]!])
|
|
233
|
+
}
|
|
234
|
+
return bindings
|
|
235
|
+
}
|
|
236
|
+
return { sql, params, bind, transitive: false }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const compileTransitive = (
|
|
240
|
+
pattern: Pattern.t,
|
|
241
|
+
verb: string,
|
|
242
|
+
subjectSlot: Pattern.Slot,
|
|
243
|
+
objectSlot: undefined | Pattern.Slot,
|
|
244
|
+
options: Options
|
|
245
|
+
): Compiled => {
|
|
246
|
+
if (pattern.negated || pattern.filters.length > 0 || pattern.contexts.length > 0 || pattern.tags.length > 0 ||
|
|
247
|
+
pattern.payload.kind === 'attribute') {
|
|
248
|
+
throw new Error('CAVE-Q: transitive patterns support subject/object slots only (spec §12.1)')
|
|
249
|
+
}
|
|
250
|
+
const base = options.all === true ? 'SELECT * FROM cave_claim' : currentSql
|
|
251
|
+
const conditions: string[] = []
|
|
252
|
+
const params: (string | number)[] = [verb]
|
|
253
|
+
if (subjectSlot.kind === 'term') {
|
|
254
|
+
conditions.push('h.src = ?')
|
|
255
|
+
params.push(subjectSlot.text)
|
|
256
|
+
}
|
|
257
|
+
if (objectSlot?.kind === 'term') {
|
|
258
|
+
conditions.push('h.dst = ?')
|
|
259
|
+
params.push(objectSlot.text)
|
|
260
|
+
}
|
|
261
|
+
// A repeated variable forces equality here just as in single-hop
|
|
262
|
+
// patterns: `?x EXTENDS+ ?x` asks for nodes on a cycle, not for every
|
|
263
|
+
// reachable pair.
|
|
264
|
+
if (subjectSlot.kind === 'var' && objectSlot?.kind === 'var' && subjectSlot.name === objectSlot.name) {
|
|
265
|
+
conditions.push('h.src = h.dst')
|
|
266
|
+
}
|
|
267
|
+
const sql = `
|
|
268
|
+
WITH RECURSIVE cur AS (
|
|
269
|
+
SELECT c.subject AS src, c.object AS dst
|
|
270
|
+
FROM (${base}) c
|
|
271
|
+
WHERE c.verb = ? AND c.negated = 0 AND c.conf > 0 AND c.object IS NOT NULL
|
|
272
|
+
), hops(src, dst, depth) AS (
|
|
273
|
+
SELECT src, dst, 1 FROM cur
|
|
274
|
+
UNION
|
|
275
|
+
SELECT h.src, cur.dst, h.depth + 1 FROM hops h JOIN cur ON cur.src = h.dst
|
|
276
|
+
WHERE h.depth < 32
|
|
277
|
+
)
|
|
278
|
+
SELECT DISTINCT h.src AS src, h.dst AS dst FROM hops h
|
|
279
|
+
${conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''}
|
|
280
|
+
ORDER BY h.src, h.dst`
|
|
281
|
+
const bind = (row: Record<string, unknown>): Record<string, string> => {
|
|
282
|
+
const bindings: Record<string, string> = {}
|
|
283
|
+
if (subjectSlot.kind === 'var') {
|
|
284
|
+
bindings[subjectSlot.name] = String(row['src'])
|
|
285
|
+
}
|
|
286
|
+
if (objectSlot?.kind === 'var') {
|
|
287
|
+
bindings[objectSlot.name] = String(row['dst'])
|
|
288
|
+
}
|
|
289
|
+
return bindings
|
|
290
|
+
}
|
|
291
|
+
return { sql, params, bind, transitive: true }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Runs a CAVE-Q query against a store.
|
|
296
|
+
*
|
|
297
|
+
* ```ts
|
|
298
|
+
* query(store, '?x USES jwt')
|
|
299
|
+
* query(store, '?cause CAUSE app/crash\n WHERE conf >= 0.7')
|
|
300
|
+
* query(store, 'terrier EXTENDS+ animal')
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
export const query = (store: Store, input: string, options: Options = {}): Match[] => {
|
|
304
|
+
const pattern = Pattern.parse(input)
|
|
305
|
+
const compiled = compile(pattern, store.registry(), options)
|
|
306
|
+
const rows = store.db.prepare(compiled.sql).all(...compiled.params) as Record<string, unknown>[]
|
|
307
|
+
return rows.map(row =>
|
|
308
|
+
compiled.transitive ?
|
|
309
|
+
{ bindings: compiled.bind(row) } :
|
|
310
|
+
{ bindings: compiled.bind(row), row: row as unknown as Row.t }
|
|
311
|
+
)
|
|
312
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cavelang/query` — CAVE-Q, the graph-pattern query layer (spec §12).
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import { query } from '@cavelang/query'
|
|
6
|
+
*
|
|
7
|
+
* query(store, '?x USES jwt') // [{ bindings: { x: 'auth/middleware' }, row }]
|
|
8
|
+
* query(store, '?x HAS bug: ?bug #security')
|
|
9
|
+
* query(store, '?x PART-OF monorepo') // inverse verbs compile to canonical rows
|
|
10
|
+
* query(store, 'terrier EXTENDS+ animal') // transitive
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export * as Pattern from './pattern.ts'
|
|
15
|
+
export { query } from './compile.ts'
|
|
16
|
+
export type { Match, Options } from './compile.ts'
|
package/src/pattern.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CAVE-Q patterns (spec §12.1).
|
|
3
|
+
*
|
|
4
|
+
* ```cave
|
|
5
|
+
* ?x USES jwt ; all systems using jwt
|
|
6
|
+
* ?x HAS bug: ?bug #security ; all security bugs
|
|
7
|
+
* ?cause CAUSE app/crash ; candidate causes
|
|
8
|
+
* WHERE conf >= 0.7
|
|
9
|
+
* ?x ?verb ?y @production ; all production facts
|
|
10
|
+
* terrier EXTENDS+ animal ; transitive hops
|
|
11
|
+
* _ USES jwt ; wildcard
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* A query is one pattern line followed by any number of `WHERE` filter
|
|
15
|
+
* lines (spec §12.2), indented or not.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Confidence, Verb } from '@cavelang/core'
|
|
19
|
+
import { Token } from '@cavelang/parser'
|
|
20
|
+
|
|
21
|
+
/** A pattern slot: named variable, wildcard `_`, or a bound term. */
|
|
22
|
+
export type Slot =
|
|
23
|
+
| { readonly kind: 'var', readonly name: string }
|
|
24
|
+
| { readonly kind: 'wildcard' }
|
|
25
|
+
| { readonly kind: 'term', readonly text: string }
|
|
26
|
+
|
|
27
|
+
/** Verb position: concrete verb (possibly transitive `VERB+`), variable, or wildcard. */
|
|
28
|
+
export type VerbSlot =
|
|
29
|
+
| { readonly kind: 'verb', readonly name: string, readonly transitive: boolean }
|
|
30
|
+
| { readonly kind: 'var', readonly name: string }
|
|
31
|
+
| { readonly kind: 'wildcard' }
|
|
32
|
+
|
|
33
|
+
export type PayloadPattern =
|
|
34
|
+
| { readonly kind: 'object', readonly object: Slot }
|
|
35
|
+
| { readonly kind: 'attribute', readonly attribute: string, readonly value: Slot }
|
|
36
|
+
| { readonly kind: 'any' }
|
|
37
|
+
|
|
38
|
+
export type FilterOp = '=' | '!=' | '>' | '>=' | '<' | '<='
|
|
39
|
+
|
|
40
|
+
export type Filter =
|
|
41
|
+
| { readonly field: 'conf', readonly op: FilterOp, readonly value: number }
|
|
42
|
+
| { readonly field: 'tag', readonly op: '=', readonly key: string, readonly value?: string }
|
|
43
|
+
| { readonly field: 'context', readonly op: '=', readonly value: string }
|
|
44
|
+
| { readonly field: 'value', readonly op: FilterOp, readonly value: number, readonly unit?: string }
|
|
45
|
+
| { readonly field: 'tx', readonly op: FilterOp, readonly value: string }
|
|
46
|
+
|
|
47
|
+
export type Pattern = {
|
|
48
|
+
readonly subject: Slot
|
|
49
|
+
readonly verb: VerbSlot
|
|
50
|
+
readonly payload: PayloadPattern
|
|
51
|
+
readonly negated: boolean
|
|
52
|
+
/** `@ctx` filters on the pattern line. */
|
|
53
|
+
readonly contexts: readonly string[]
|
|
54
|
+
/** `#tag[:value]` filters on the pattern line. */
|
|
55
|
+
readonly tags: readonly { key: string, value?: string }[]
|
|
56
|
+
readonly filters: readonly Filter[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type t = Pattern
|
|
60
|
+
|
|
61
|
+
const filterOps: readonly FilterOp[] = ['>=', '<=', '!=', '=', '>', '<']
|
|
62
|
+
|
|
63
|
+
const isFilterOp = (text: string): text is FilterOp =>
|
|
64
|
+
filterOps.includes(text as FilterOp)
|
|
65
|
+
|
|
66
|
+
const slotOf = (token: Token.t): Slot => {
|
|
67
|
+
if (token.kind === 'word') {
|
|
68
|
+
if (token.text === '_') {
|
|
69
|
+
return { kind: 'wildcard' }
|
|
70
|
+
}
|
|
71
|
+
if (token.text.startsWith('?')) {
|
|
72
|
+
return { kind: 'var', name: token.text.slice(1) }
|
|
73
|
+
}
|
|
74
|
+
return { kind: 'term', text: token.text }
|
|
75
|
+
}
|
|
76
|
+
const delimiter = token.kind === 'code' ? '`' : '"'
|
|
77
|
+
return { kind: 'term', text: `${delimiter}${token.text}${delimiter}` }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const verbSlotOf = (token: Token.t): undefined | VerbSlot => {
|
|
81
|
+
if (token.kind !== 'word') {
|
|
82
|
+
return undefined
|
|
83
|
+
}
|
|
84
|
+
if (token.text === '_') {
|
|
85
|
+
return { kind: 'wildcard' }
|
|
86
|
+
}
|
|
87
|
+
if (token.text.startsWith('?')) {
|
|
88
|
+
return { kind: 'var', name: token.text.slice(1) }
|
|
89
|
+
}
|
|
90
|
+
const transitive = token.text.endsWith('+')
|
|
91
|
+
const name = transitive ? token.text.slice(0, -1) : token.text
|
|
92
|
+
return Verb.isVerbToken(name) ? { kind: 'verb', name, transitive } : undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const parseFilter = (tokens: readonly Token.t[], lineNo: number): Filter => {
|
|
96
|
+
const [field, op, ...valueTokens] = tokens
|
|
97
|
+
const bad = (message: string): never => {
|
|
98
|
+
throw new Error(`CAVE-Q line ${lineNo}: ${message}`)
|
|
99
|
+
}
|
|
100
|
+
if (field?.kind !== 'word' || op?.kind !== 'word' || valueTokens.length === 0) {
|
|
101
|
+
return bad('expected "WHERE <field> <op> <value>" (spec §12.2)')
|
|
102
|
+
}
|
|
103
|
+
if (!isFilterOp(op.text)) {
|
|
104
|
+
return bad(`unknown operator ${JSON.stringify(op.text)}`)
|
|
105
|
+
}
|
|
106
|
+
const valueText = valueTokens.map(token => token.text).join(' ')
|
|
107
|
+
switch (field.text) {
|
|
108
|
+
case 'conf': {
|
|
109
|
+
const conf = valueText.endsWith('%') ? Confidence.parse(valueText) : Number(valueText)
|
|
110
|
+
if (conf === undefined || Number.isNaN(conf)) {
|
|
111
|
+
return bad(`cannot parse confidence ${JSON.stringify(valueText)}`)
|
|
112
|
+
}
|
|
113
|
+
return { field: 'conf', op: op.text, value: conf }
|
|
114
|
+
}
|
|
115
|
+
case 'tag': {
|
|
116
|
+
if (op.text !== '=') {
|
|
117
|
+
return bad('tag filters support = only')
|
|
118
|
+
}
|
|
119
|
+
const colonAt = valueText.indexOf(':')
|
|
120
|
+
return colonAt === -1 ?
|
|
121
|
+
{ field: 'tag', op: '=', key: valueText } :
|
|
122
|
+
{ field: 'tag', op: '=', key: valueText.slice(0, colonAt), value: valueText.slice(colonAt + 1) }
|
|
123
|
+
}
|
|
124
|
+
case 'context': {
|
|
125
|
+
if (op.text !== '=') {
|
|
126
|
+
return bad('context filters support = only')
|
|
127
|
+
}
|
|
128
|
+
return { field: 'context', op: '=', value: valueText }
|
|
129
|
+
}
|
|
130
|
+
case 'value': {
|
|
131
|
+
const match = /^(-?\d+(?:\.\d+)?)(?:\s+(.+))?$/.exec(valueText)
|
|
132
|
+
if (!match) {
|
|
133
|
+
return bad(`cannot parse value filter ${JSON.stringify(valueText)}`)
|
|
134
|
+
}
|
|
135
|
+
const unit = match[2]
|
|
136
|
+
return { field: 'value', op: op.text, value: Number(match[1]), ...unit === undefined ? {} : { unit } }
|
|
137
|
+
}
|
|
138
|
+
case 'tx':
|
|
139
|
+
return { field: 'tx', op: op.text, value: valueText }
|
|
140
|
+
default:
|
|
141
|
+
return bad(`unknown filter field ${JSON.stringify(field.text)}`)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Parses a CAVE-Q query: one pattern line plus `WHERE` filter lines. */
|
|
146
|
+
export const parse = (input: string): Pattern => {
|
|
147
|
+
const lines = input
|
|
148
|
+
.split(/\r?\n/)
|
|
149
|
+
.map(line => line.trim())
|
|
150
|
+
.filter(line => line !== '' && !line.startsWith(';'))
|
|
151
|
+
if (lines.length === 0) {
|
|
152
|
+
throw new Error('CAVE-Q: empty query')
|
|
153
|
+
}
|
|
154
|
+
const [patternLine, ...filterLines] = lines
|
|
155
|
+
const { head } = Token.splitComment(patternLine!)
|
|
156
|
+
const tokens = Token.tokenize(head)
|
|
157
|
+
if (tokens.length < 2) {
|
|
158
|
+
throw new Error('CAVE-Q: a pattern needs at least a subject and a verb (spec §12.1)')
|
|
159
|
+
}
|
|
160
|
+
const subject = slotOf(tokens[0]!)
|
|
161
|
+
const verb = verbSlotOf(tokens[1]!)
|
|
162
|
+
if (verb === undefined) {
|
|
163
|
+
throw new Error(`CAVE-Q: cannot parse verb position ${JSON.stringify(tokens[1]!.text)}`)
|
|
164
|
+
}
|
|
165
|
+
let rest = tokens.slice(2)
|
|
166
|
+
const negated = rest[0]?.kind === 'word' && rest[0].text === 'NOT'
|
|
167
|
+
if (negated) {
|
|
168
|
+
rest = rest.slice(1)
|
|
169
|
+
}
|
|
170
|
+
const contexts: string[] = []
|
|
171
|
+
const tags: { key: string, value?: string }[] = []
|
|
172
|
+
const payloadTokens: Token.t[] = []
|
|
173
|
+
for (const token of rest) {
|
|
174
|
+
if (token.kind === 'word' && token.text.startsWith('@') && token.text.length > 1) {
|
|
175
|
+
contexts.push(token.text.slice(1))
|
|
176
|
+
} else if (token.kind === 'word' && token.text.startsWith('#') && token.text.length > 1) {
|
|
177
|
+
const body = token.text.slice(1)
|
|
178
|
+
const colonAt = body.indexOf(':')
|
|
179
|
+
tags.push(colonAt === -1 ? { key: body } : { key: body.slice(0, colonAt), value: body.slice(colonAt + 1) })
|
|
180
|
+
} else {
|
|
181
|
+
payloadTokens.push(token)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let payload: PayloadPattern
|
|
185
|
+
if (payloadTokens.length === 0) {
|
|
186
|
+
payload = { kind: 'any' }
|
|
187
|
+
} else if (payloadTokens[0]!.kind === 'word' && payloadTokens[0]!.text.endsWith(':') && payloadTokens[0]!.text.length > 1) {
|
|
188
|
+
if (payloadTokens.length !== 2) {
|
|
189
|
+
throw new Error('CAVE-Q: attribute patterns take exactly one value slot (spec §12.1)')
|
|
190
|
+
}
|
|
191
|
+
payload = { kind: 'attribute', attribute: payloadTokens[0]!.text.slice(0, -1), value: slotOf(payloadTokens[1]!) }
|
|
192
|
+
} else if (payloadTokens.length === 1) {
|
|
193
|
+
payload = { kind: 'object', object: slotOf(payloadTokens[0]!) }
|
|
194
|
+
} else {
|
|
195
|
+
throw new Error(`CAVE-Q: cannot parse payload ${JSON.stringify(payloadTokens.map(token => token.text).join(' '))}`)
|
|
196
|
+
}
|
|
197
|
+
const filters = filterLines.map((line, at) => {
|
|
198
|
+
const filterTokens = Token.tokenize(Token.splitComment(line).head)
|
|
199
|
+
const [keyword, ...rest_] = filterTokens
|
|
200
|
+
if (keyword?.kind !== 'word' || keyword.text !== 'WHERE') {
|
|
201
|
+
throw new Error(`CAVE-Q line ${at + 2}: expected WHERE, got ${JSON.stringify(keyword?.text ?? '')}`)
|
|
202
|
+
}
|
|
203
|
+
return parseFilter(rest_, at + 2)
|
|
204
|
+
})
|
|
205
|
+
return { subject, verb, payload, negated, contexts, tags, filters }
|
|
206
|
+
}
|