@cavelang/query 0.35.0 → 0.36.1
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 +498 -9
- package/dist/src/bounded.d.ts +1 -1
- package/dist/src/bounded.d.ts.map +1 -1
- package/dist/src/bounded.js +14 -7
- package/dist/src/bounded.js.map +1 -1
- package/dist/src/capture.d.ts +7 -0
- package/dist/src/capture.d.ts.map +1 -0
- package/dist/src/capture.js +52 -0
- package/dist/src/capture.js.map +1 -0
- package/dist/src/compile.d.ts +3 -3
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +107 -54
- package/dist/src/compile.js.map +1 -1
- package/dist/src/page.d.ts.map +1 -1
- package/dist/src/page.js +103 -45
- package/dist/src/page.js.map +1 -1
- package/dist/src/pattern.d.ts.map +1 -1
- package/dist/src/pattern.js +20 -10
- package/dist/src/pattern.js.map +1 -1
- package/dist/src/read-snapshot.d.ts +4 -0
- package/dist/src/read-snapshot.d.ts.map +1 -0
- package/dist/src/read-snapshot.js +29 -0
- package/dist/src/read-snapshot.js.map +1 -0
- package/dist/src/record.d.ts.map +1 -1
- package/dist/src/record.js +51 -16
- package/dist/src/record.js.map +1 -1
- package/dist/src/unicode.d.ts +3 -0
- package/dist/src/unicode.d.ts.map +1 -0
- package/dist/src/unicode.js +7 -0
- package/dist/src/unicode.js.map +1 -0
- package/package.json +6 -6
- package/src/bounded.ts +23 -8
- package/src/capture.ts +59 -0
- package/src/compile.ts +102 -56
- package/src/page.ts +102 -44
- package/src/pattern.ts +20 -10
- package/src/read-snapshot.ts +23 -0
- package/src/record.ts +52 -16
- package/src/unicode.ts +6 -0
package/src/pattern.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { Confidence, Value, Verb } from '@cavelang/core'
|
|
19
19
|
import { Token } from '@cavelang/parser'
|
|
20
|
+
import { assertQueryUnicode } from './unicode.ts'
|
|
20
21
|
|
|
21
22
|
/** A pattern slot: named variable, wildcard `_`, or a bound term. */
|
|
22
23
|
export type Slot =
|
|
@@ -63,13 +64,19 @@ const filterOps: readonly FilterOp[] = ['>=', '<=', '!=', '=', '>', '<']
|
|
|
63
64
|
const isFilterOp = (text: string): text is FilterOp =>
|
|
64
65
|
filterOps.includes(text as FilterOp)
|
|
65
66
|
|
|
67
|
+
const variableName = (text: string): string => {
|
|
68
|
+
const name = text.slice(1)
|
|
69
|
+
if (name === '') throw new Error('CAVE-Q: variable requires a name after ?; use _ for a wildcard')
|
|
70
|
+
return name
|
|
71
|
+
}
|
|
72
|
+
|
|
66
73
|
const slotOf = (token: Token.t): Slot => {
|
|
67
74
|
if (token.kind === 'word') {
|
|
68
75
|
if (token.text === '_') {
|
|
69
76
|
return { kind: 'wildcard' }
|
|
70
77
|
}
|
|
71
78
|
if (token.text.startsWith('?')) {
|
|
72
|
-
return { kind: 'var', name: token.text
|
|
79
|
+
return { kind: 'var', name: variableName(token.text) }
|
|
73
80
|
}
|
|
74
81
|
return { kind: 'term', text: token.text }
|
|
75
82
|
}
|
|
@@ -85,7 +92,7 @@ const verbSlotOf = (token: Token.t): undefined | VerbSlot => {
|
|
|
85
92
|
return { kind: 'wildcard' }
|
|
86
93
|
}
|
|
87
94
|
if (token.text.startsWith('?')) {
|
|
88
|
-
return { kind: 'var', name: token.text
|
|
95
|
+
return { kind: 'var', name: variableName(token.text) }
|
|
89
96
|
}
|
|
90
97
|
const transitive = token.text.endsWith('+')
|
|
91
98
|
const name = transitive ? token.text.slice(0, -1) : token.text
|
|
@@ -107,7 +114,7 @@ const parseFilter = (tokens: readonly Token.t[], lineNo: number): Filter => {
|
|
|
107
114
|
switch (field.text) {
|
|
108
115
|
case 'conf': {
|
|
109
116
|
const conf = valueText.endsWith('%') ? Confidence.parse(valueText) : Number(valueText)
|
|
110
|
-
if (conf === undefined || Number.
|
|
117
|
+
if (conf === undefined || !Number.isFinite(conf)) {
|
|
111
118
|
return bad(`cannot parse confidence ${JSON.stringify(valueText)}`)
|
|
112
119
|
}
|
|
113
120
|
return { field: 'conf', op: op.text, value: conf }
|
|
@@ -148,13 +155,16 @@ const parseFilter = (tokens: readonly Token.t[], lineNo: number): Filter => {
|
|
|
148
155
|
export const parse = (input: string): Pattern => {
|
|
149
156
|
const lines = input
|
|
150
157
|
.split(/\r?\n/)
|
|
151
|
-
.map(line =>
|
|
152
|
-
|
|
158
|
+
.map((line, index) => {
|
|
159
|
+
assertQueryUnicode(line, index + 1)
|
|
160
|
+
return { text: line.trim(), lineNo: index + 1 }
|
|
161
|
+
})
|
|
162
|
+
.filter(line => line.text !== '' && !line.text.startsWith(';'))
|
|
153
163
|
if (lines.length === 0) {
|
|
154
164
|
throw new Error('CAVE-Q: empty query')
|
|
155
165
|
}
|
|
156
166
|
const [patternLine, ...filterLines] = lines
|
|
157
|
-
const { head } = Token.splitComment(patternLine
|
|
167
|
+
const { head } = Token.splitComment(patternLine!.text)
|
|
158
168
|
const tokens = Token.tokenize(head)
|
|
159
169
|
if (tokens.length < 2) {
|
|
160
170
|
throw new Error('CAVE-Q: a pattern needs at least a subject and a verb (spec §12.1)')
|
|
@@ -214,13 +224,13 @@ export const parse = (input: string): Pattern => {
|
|
|
214
224
|
} else {
|
|
215
225
|
throw new Error(`CAVE-Q: cannot parse payload ${JSON.stringify(payloadTokens.map(token => token.text).join(' '))}`)
|
|
216
226
|
}
|
|
217
|
-
const filters = filterLines.map(
|
|
218
|
-
const filterTokens = Token.tokenize(Token.splitComment(line).head)
|
|
227
|
+
const filters = filterLines.map(line => {
|
|
228
|
+
const filterTokens = Token.tokenize(Token.splitComment(line.text).head)
|
|
219
229
|
const [keyword, ...rest_] = filterTokens
|
|
220
230
|
if (keyword?.kind !== 'word' || keyword.text !== 'WHERE') {
|
|
221
|
-
throw new Error(`CAVE-Q line ${
|
|
231
|
+
throw new Error(`CAVE-Q line ${line.lineNo}: expected WHERE, got ${JSON.stringify(keyword?.text ?? '')}`)
|
|
222
232
|
}
|
|
223
|
-
return parseFilter(rest_,
|
|
233
|
+
return parseFilter(rest_, line.lineNo)
|
|
224
234
|
})
|
|
225
235
|
return { subject, verb, payload, negated, contexts, tags, filters }
|
|
226
236
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Store } from '@cavelang/store/adapter'
|
|
2
|
+
|
|
3
|
+
const message = (error: unknown): string => {
|
|
4
|
+
try { return error instanceof Error ? String(error.message) : String(error) }
|
|
5
|
+
catch { return '[unprintable thrown value]' }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** A deferred savepoint also nests within a caller-owned transaction. */
|
|
9
|
+
export const readSnapshot = <T>(store: Store, read: () => T): T => {
|
|
10
|
+
const savepoint = 'cave_query_read'
|
|
11
|
+
store.db.exec(`SAVEPOINT ${savepoint}`)
|
|
12
|
+
let result: T
|
|
13
|
+
try { result = read() } catch (error) {
|
|
14
|
+
try { store.db.exec(`RELEASE ${savepoint}`) } catch (releaseError) {
|
|
15
|
+
throw new AggregateError([error, releaseError],
|
|
16
|
+
`CAVE-Q matching failed: ${message(error)}; read snapshot release also failed: ${message(releaseError)}`,
|
|
17
|
+
{ cause: error })
|
|
18
|
+
}
|
|
19
|
+
throw error
|
|
20
|
+
}
|
|
21
|
+
store.db.exec(`RELEASE ${savepoint}`)
|
|
22
|
+
return result
|
|
23
|
+
}
|
package/src/record.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { Record as ClaimRecord } from '@cavelang/store/adapter'
|
|
|
4
4
|
import type { Store } from '@cavelang/store/adapter'
|
|
5
5
|
import type { Match, Options } from './compile.ts'
|
|
6
6
|
import { query } from './bounded.ts'
|
|
7
|
+
import { readSnapshot } from './read-snapshot.ts'
|
|
8
|
+
import { captureOptions } from './capture.ts'
|
|
7
9
|
|
|
8
10
|
export const format = 'cave.query-match' as const
|
|
9
11
|
export const version = 1 as const
|
|
@@ -17,38 +19,69 @@ export type t = {
|
|
|
17
19
|
readonly at?: Match['at']
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
export const of = (store: Store, match: Match): t =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
22
|
+
export const of = (store: Store, match: Match): t => {
|
|
23
|
+
const supplied = { bindings: match.bindings, row: match.row, rows: match.rows, at: match.at }
|
|
24
|
+
requireBindingPrototype(supplied.bindings)
|
|
25
|
+
const { bindings, row, rows, at } = structuredClone(supplied)
|
|
26
|
+
validateMetadata(bindings, rows, at)
|
|
27
|
+
if (rows !== undefined) for (const evidence of rows) {
|
|
28
|
+
if (!object(evidence)) throw new Error(`CAVE query record: malformed ${format}/v${version} support`)
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
format,
|
|
32
|
+
version,
|
|
33
|
+
bindings,
|
|
34
|
+
...(row === undefined ? {} : { claim: store.recordOf(row) }),
|
|
35
|
+
...(rows === undefined ? {} : { support: rows.map(store.recordOf) }),
|
|
36
|
+
...(at === undefined ? {} : { at }),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
28
39
|
|
|
29
40
|
const object = (value: unknown): value is Record<string, unknown> =>
|
|
30
41
|
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
31
42
|
|
|
43
|
+
const requireBindingPrototype = (bindings: unknown): void => {
|
|
44
|
+
if (object(bindings) && Object.getPrototypeOf(bindings) !== Object.prototype &&
|
|
45
|
+
Object.getPrototypeOf(bindings) !== null) {
|
|
46
|
+
throw new Error(`CAVE query record: malformed ${format}/v${version}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const validateMetadata = (bindings: unknown, support: unknown, at: unknown): void => {
|
|
51
|
+
if (!object(bindings) ||
|
|
52
|
+
(Object.getPrototypeOf(bindings) !== Object.prototype && Object.getPrototypeOf(bindings) !== null) ||
|
|
53
|
+
!Object.values(bindings).every(binding => typeof binding === 'string') ||
|
|
54
|
+
(support !== undefined && !Array.isArray(support))) {
|
|
55
|
+
throw new Error(`CAVE query record: malformed ${format}/v${version}`)
|
|
56
|
+
}
|
|
57
|
+
if (at !== undefined && (!object(at) || typeof at['num'] !== 'number' || !Number.isFinite(at['num']) ||
|
|
58
|
+
typeof at['text'] !== 'string' || (at['unit'] !== undefined && typeof at['unit'] !== 'string'))) {
|
|
59
|
+
throw new Error(`CAVE query record: malformed ${format}/v${version} interpolation`)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
32
63
|
/** Decode a persisted query-match record and every nested claim record. */
|
|
33
64
|
export const decode = (input: string | unknown): t => {
|
|
34
|
-
|
|
65
|
+
let value: unknown = typeof input === 'string' ? JSON.parse(input) : object(input) ? { ...input } : input
|
|
66
|
+
if (typeof input !== 'string' && object(value)) {
|
|
67
|
+
// Check the supplied map before cloning normalizes custom prototypes.
|
|
68
|
+
requireBindingPrototype(value['bindings'])
|
|
69
|
+
value = structuredClone(value)
|
|
70
|
+
}
|
|
35
71
|
if (!object(value) || value['format'] !== format) {
|
|
36
72
|
throw new Error(`CAVE query record: expected format ${JSON.stringify(format)}`)
|
|
37
73
|
}
|
|
38
74
|
if (value['version'] !== version) {
|
|
39
75
|
throw new Error(`CAVE query record: unsupported ${format} version ${JSON.stringify(value['version'])}`)
|
|
40
76
|
}
|
|
41
|
-
|
|
42
|
-
!Object.values(value['bindings']).every(binding => typeof binding === 'string') ||
|
|
43
|
-
(value['support'] !== undefined && !Array.isArray(value['support']))) {
|
|
44
|
-
throw new Error(`CAVE query record: malformed ${format}/v${version}`)
|
|
45
|
-
}
|
|
77
|
+
validateMetadata(value['bindings'], value['support'], value['at'])
|
|
46
78
|
const decoded = value as t
|
|
47
79
|
return {
|
|
48
80
|
...decoded,
|
|
49
81
|
...(decoded.claim === undefined ? {} : { claim: ClaimRecord.decode(decoded.claim) }),
|
|
50
82
|
...(decoded.support === undefined ? {} : {
|
|
51
|
-
support: decoded.support.
|
|
83
|
+
support: Array.from({ length: decoded.support.length },
|
|
84
|
+
(_, index) => ClaimRecord.decode(decoded.support![index]))
|
|
52
85
|
}),
|
|
53
86
|
}
|
|
54
87
|
}
|
|
@@ -61,4 +94,7 @@ export const queryRecords = (
|
|
|
61
94
|
store: Store,
|
|
62
95
|
input: string,
|
|
63
96
|
options: Options = {}
|
|
64
|
-
): t[] =>
|
|
97
|
+
): t[] => {
|
|
98
|
+
options = captureOptions(options)
|
|
99
|
+
return readSnapshot(store, () => query(store, input, options).map(match => of(store, match)))
|
|
100
|
+
}
|
package/src/unicode.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Query strings must survive SQLite's UTF-8 encoding without replacement. */
|
|
2
|
+
export const assertQueryUnicode = (value: unknown, line?: number): void => {
|
|
3
|
+
if (typeof value === 'string' && /[\uD800-\uDFFF]/u.test(value)) {
|
|
4
|
+
throw new Error(`CAVE-Q${line === undefined ? '' : ` line ${line}`}: unpaired UTF-16 surrogate cannot be queried as UTF-8`)
|
|
5
|
+
}
|
|
6
|
+
}
|