@hypequery/datasets 0.12.7 → 0.13.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/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/sql-portability.d.ts +42 -0
- package/dist/sql-portability.d.ts.map +1 -0
- package/dist/sql-portability.js +641 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type { DatasetCatalog, DatasetCatalogMap, DatasetCatalogSource, Dimension
|
|
|
12
12
|
export { serializeSemanticContract, contractToStableJson, hashContract, SEMANTIC_CONTRACT_VERSION, } from './contract.js';
|
|
13
13
|
export { buildProtocolDatasetContract } from './protocol-adapter.js';
|
|
14
14
|
export type { BuildProtocolDatasetContractOptions } from './protocol-adapter.js';
|
|
15
|
+
export { compilePortableSqlExpression, DEFAULT_SQL_PORTABILITY_LIMITS, } from './sql-portability.js';
|
|
16
|
+
export type { SqlPortabilityIssue, SqlPortabilityIssueCode, SqlPortabilityLimits, SqlPortabilityOptions, SqlPortabilityResult, } from './sql-portability.js';
|
|
15
17
|
export type { SemanticContract, SerializeSemanticContractOptions, ContractDataset, ContractDimension, ContractMeasure, ContractMetric, ContractFilter, ContractRelationship, } from './contract.js';
|
|
16
18
|
export { generateDatasetTools, toOpenAITools, toAISDKTools, toMcpTools, } from './tools.js';
|
|
17
19
|
export type { AISDKToolDefinition, DatasetToolAnalytics, DatasetToolMode, GenerateDatasetToolsOptions, JsonSchema, McpToolDefinition, OpenAIToolDefinition, SemanticToolDefinition, } from './tools.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAGvC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAGhE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAGnI,OAAO,EACL,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAC/B,UAAU,EAAE,QAAQ,EACpB,KAAK,EAAE,KAAK,EAAE,IAAI,GACnB,MAAM,eAAe,CAAC;AAGvB,OAAO,EACL,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EACzB,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAChC,GAAG,EAAE,IAAI,EACT,MAAM,EAAE,KAAK,GACd,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAGtD,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,8BAA8B,EAAE,MAAM,cAAc,CAAC;AACrG,OAAO,EAAE,+BAA+B,EAAE,MAAM,gCAAgC,CAAC;AACjF,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,GACzB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,yBAAyB,EACzB,oBAAoB,EACpB,YAAY,EACZ,yBAAyB,GAC1B,MAAM,eAAe,CAAC;AAGvB,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AACrE,YAAY,EAAE,mCAAmC,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,8 @@ export { listQueryableRelationshipFields } from './utils/relationship-fields.js'
|
|
|
21
21
|
export { serializeSemanticContract, contractToStableJson, hashContract, SEMANTIC_CONTRACT_VERSION, } from './contract.js';
|
|
22
22
|
// Portable deployment contract adapter
|
|
23
23
|
export { buildProtocolDatasetContract } from './protocol-adapter.js';
|
|
24
|
+
// SQL portability compiler (R1A-07)
|
|
25
|
+
export { compilePortableSqlExpression, DEFAULT_SQL_PORTABILITY_LIMITS, } from './sql-portability.js';
|
|
24
26
|
// Agent/tool metadata
|
|
25
27
|
export { generateDatasetTools, toOpenAITools, toAISDKTools, toMcpTools, } from './tools.js';
|
|
26
28
|
// Dataset client
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ProtocolExpression } from '@hypequery/protocol';
|
|
2
|
+
/**
|
|
3
|
+
* SQL portability compiler (R1A-07).
|
|
4
|
+
*
|
|
5
|
+
* Parses the supported ClickHouse SQL-expression subset used by SQL-backed
|
|
6
|
+
* dimensions and measures into an RFC 0003 expression AST. Unsupported syntax
|
|
7
|
+
* becomes a source-located incompatibility issue instead of executing or
|
|
8
|
+
* silently changing meaning.
|
|
9
|
+
*/
|
|
10
|
+
export type SqlPortabilityIssueCode = 'HQ_SQL_PORT_SYNTAX' | 'HQ_SQL_PORT_UNSUPPORTED_FUNCTION' | 'HQ_SQL_PORT_UNSUPPORTED_OPERATOR' | 'HQ_SQL_PORT_UNSUPPORTED_LITERAL' | 'HQ_SQL_PORT_UNSUPPORTED_SYNTAX' | 'HQ_SQL_PORT_TOO_COMPLEX' | 'HQ_SQL_PORT_TOO_LARGE';
|
|
11
|
+
export interface SqlPortabilityIssue {
|
|
12
|
+
readonly code: SqlPortabilityIssueCode;
|
|
13
|
+
readonly message: string;
|
|
14
|
+
readonly start: number;
|
|
15
|
+
readonly end: number;
|
|
16
|
+
}
|
|
17
|
+
export interface SqlPortabilityLimits {
|
|
18
|
+
readonly maxInputBytes: number;
|
|
19
|
+
readonly maxDepth: number;
|
|
20
|
+
readonly maxNodes: number;
|
|
21
|
+
}
|
|
22
|
+
export interface SqlPortabilityOptions {
|
|
23
|
+
readonly limits?: Partial<SqlPortabilityLimits>;
|
|
24
|
+
}
|
|
25
|
+
export interface SqlPortabilitySuccess {
|
|
26
|
+
readonly portable: true;
|
|
27
|
+
readonly expression: ProtocolExpression;
|
|
28
|
+
readonly dependencies: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
export interface SqlPortabilityFailure {
|
|
31
|
+
readonly portable: false;
|
|
32
|
+
readonly issues: readonly SqlPortabilityIssue[];
|
|
33
|
+
}
|
|
34
|
+
export type SqlPortabilityResult = SqlPortabilitySuccess | SqlPortabilityFailure;
|
|
35
|
+
export declare const DEFAULT_SQL_PORTABILITY_LIMITS: Readonly<SqlPortabilityLimits>;
|
|
36
|
+
/**
|
|
37
|
+
* Compile a SQL expression fragment into an RFC 0003 expression. The result
|
|
38
|
+
* expression always passes RFC 0003 validation; dependencies are the sorted,
|
|
39
|
+
* deduplicated referenced identifiers.
|
|
40
|
+
*/
|
|
41
|
+
export declare function compilePortableSqlExpression(sql: string, options?: SqlPortabilityOptions): SqlPortabilityResult;
|
|
42
|
+
//# sourceMappingURL=sql-portability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-portability.d.ts","sourceRoot":"","sources":["../src/sql-portability.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,kBAAkB,EAExB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;GAOG;AAEH,MAAM,MAAM,uBAAuB,GAC/B,oBAAoB,GACpB,kCAAkC,GAClC,kCAAkC,GAClC,iCAAiC,GACjC,gCAAgC,GAChC,yBAAyB,GACzB,uBAAuB,CAAC;AAE5B,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;CACjD;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,kBAAkB,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,SAAS,mBAAmB,EAAE,CAAC;CACjD;AAED,MAAM,MAAM,oBAAoB,GAAG,qBAAqB,GAAG,qBAAqB,CAAC;AAEjF,eAAO,MAAM,8BAA8B,EAAE,QAAQ,CAAC,oBAAoB,CAKtE,CAAC;AAouBL;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,qBAA0B,GAClC,oBAAoB,CAyBtB"}
|
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
import { parseProtocolQualifiedIdentifier, ProtocolIdentifierError, validateProtocolExpression, } from '@hypequery/protocol';
|
|
2
|
+
export const DEFAULT_SQL_PORTABILITY_LIMITS = Object.freeze({
|
|
3
|
+
maxInputBytes: 65_536,
|
|
4
|
+
maxDepth: 16,
|
|
5
|
+
maxNodes: 1_000,
|
|
6
|
+
});
|
|
7
|
+
function resolveSqlPortabilityLimits(options = {}) {
|
|
8
|
+
const limits = { ...DEFAULT_SQL_PORTABILITY_LIMITS };
|
|
9
|
+
for (const key of Object.keys(limits)) {
|
|
10
|
+
const value = options.limits?.[key];
|
|
11
|
+
if (value === undefined)
|
|
12
|
+
continue;
|
|
13
|
+
const maximum = DEFAULT_SQL_PORTABILITY_LIMITS[key];
|
|
14
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
15
|
+
throw new RangeError(`${key} must be a positive safe integer no greater than ${maximum} `
|
|
16
|
+
+ '(the SQL portability v1 maximum)');
|
|
17
|
+
}
|
|
18
|
+
limits[key] = value;
|
|
19
|
+
}
|
|
20
|
+
return Object.freeze(limits);
|
|
21
|
+
}
|
|
22
|
+
class SqlPortabilityError extends Error {
|
|
23
|
+
issue;
|
|
24
|
+
constructor(issue) {
|
|
25
|
+
super(issue.message);
|
|
26
|
+
this.name = 'SqlPortabilityError';
|
|
27
|
+
this.issue = issue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function fail(code, message, start, end) {
|
|
31
|
+
throw new SqlPortabilityError({ code, message, start, end });
|
|
32
|
+
}
|
|
33
|
+
const BARE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*/;
|
|
34
|
+
/** Keywords with direct AST support, keyed by their uppercase spelling. */
|
|
35
|
+
const KEYWORDS = Object.freeze({
|
|
36
|
+
AND: 'and',
|
|
37
|
+
OR: 'or',
|
|
38
|
+
NOT: 'not',
|
|
39
|
+
IN: 'in',
|
|
40
|
+
BETWEEN: 'between',
|
|
41
|
+
LIKE: 'like',
|
|
42
|
+
TRUE: 'true',
|
|
43
|
+
FALSE: 'false',
|
|
44
|
+
NULL: 'null',
|
|
45
|
+
});
|
|
46
|
+
/** Reserved words that prove the input is not a portable expression. */
|
|
47
|
+
const UNSUPPORTED_KEYWORDS = new Set([
|
|
48
|
+
'ALTER', 'AS', 'CASE', 'CAST', 'CREATE', 'DATABASE', 'DELETE', 'DROP',
|
|
49
|
+
'ELSE', 'END', 'EXCEPT', 'EXISTS', 'FORMAT', 'FROM', 'GROUP', 'HAVING',
|
|
50
|
+
'INSERT', 'INTERSECT', 'INTO', 'JOIN', 'LAMBDA', 'LIMIT', 'ORDER', 'OVER',
|
|
51
|
+
'PARTITION', 'PREWHERE', 'SELECT', 'SETTINGS', 'TABLE', 'THEN', 'TRUNCATE',
|
|
52
|
+
'UNION', 'UPDATE', 'WHEN', 'WHERE', 'WINDOW', 'WITH',
|
|
53
|
+
]);
|
|
54
|
+
const BARE_IDENTIFIER_FULL = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
55
|
+
function tokenize(sql) {
|
|
56
|
+
const tokens = [];
|
|
57
|
+
let index = 0;
|
|
58
|
+
const push = (type, value, start, end) => {
|
|
59
|
+
tokens.push({ type, value, start, end });
|
|
60
|
+
};
|
|
61
|
+
while (index < sql.length) {
|
|
62
|
+
const start = index;
|
|
63
|
+
const char = sql[index];
|
|
64
|
+
if (char === ' ' || char === '\t' || char === '\n' || char === '\r') {
|
|
65
|
+
index += 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (char === '-' && sql[index + 1] === '-') {
|
|
69
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_SYNTAX', 'Comments are not portable.', start, start + 2);
|
|
70
|
+
}
|
|
71
|
+
if (char === '/' && sql[index + 1] === '*') {
|
|
72
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_SYNTAX', 'Comments are not portable.', start, start + 2);
|
|
73
|
+
}
|
|
74
|
+
if ('(),'.includes(char)) {
|
|
75
|
+
push('operator', char, start, start + 1);
|
|
76
|
+
index += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (char === '+' || char === '-' || char === '*' || char === '/') {
|
|
80
|
+
push('operator', char, start, start + 1);
|
|
81
|
+
index += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (char === '=' && sql[index + 1] === '=') {
|
|
85
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_OPERATOR', 'Operator "==" is not portable; use "=" for equality.', start, start + 2);
|
|
86
|
+
}
|
|
87
|
+
if (char === '=') {
|
|
88
|
+
push('operator', '=', start, start + 1);
|
|
89
|
+
index += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (char === '!' && sql[index + 1] === '=') {
|
|
93
|
+
push('operator', '!=', start, start + 2);
|
|
94
|
+
index += 2;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (char === '<' && sql[index + 1] === '>') {
|
|
98
|
+
push('operator', '!=', start, start + 2);
|
|
99
|
+
index += 2;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (char === '<' && sql[index + 1] === '=') {
|
|
103
|
+
push('operator', '<=', start, start + 2);
|
|
104
|
+
index += 2;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (char === '>' && sql[index + 1] === '=') {
|
|
108
|
+
push('operator', '>=', start, start + 2);
|
|
109
|
+
index += 2;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (char === '<' || char === '>') {
|
|
113
|
+
push('operator', char, start, start + 1);
|
|
114
|
+
index += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (char === '%' || char === '^' || char === '&' || char === '|'
|
|
118
|
+
|| char === '~' || char === ':' || char === '?' || char === '@') {
|
|
119
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_OPERATOR', `Operator "${char}" is not portable.`, start, start + 1);
|
|
120
|
+
}
|
|
121
|
+
if (char === ';' || char === '[' || char === ']' || char === '{' || char === '}'
|
|
122
|
+
|| char === '"' || char === '$' || char === '#') {
|
|
123
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_SYNTAX', `Syntax "${char}" is not portable.`, start, start + 1);
|
|
124
|
+
}
|
|
125
|
+
if (char === '\'') {
|
|
126
|
+
index += 1;
|
|
127
|
+
let value = '';
|
|
128
|
+
let closed = false;
|
|
129
|
+
while (index < sql.length) {
|
|
130
|
+
const current = sql[index];
|
|
131
|
+
if (current === '\'') {
|
|
132
|
+
if (sql[index + 1] === '\'') {
|
|
133
|
+
value += '\'';
|
|
134
|
+
index += 2;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
closed = true;
|
|
138
|
+
index += 1;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
if (current === '\\') {
|
|
142
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_LITERAL', 'Backslash escapes are not portable; double the quote instead.', index, index + 1);
|
|
143
|
+
}
|
|
144
|
+
value += current;
|
|
145
|
+
index += 1;
|
|
146
|
+
}
|
|
147
|
+
if (!closed)
|
|
148
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Unterminated string literal.', start, sql.length);
|
|
149
|
+
push('string', value, start, index);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (/[0-9]/.test(char) || (char === '.' && /[0-9]/.test(sql[index + 1] ?? ''))) {
|
|
153
|
+
const match = /^[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?|^\.[0-9]+(?:[eE][+-]?[0-9]+)?/
|
|
154
|
+
.exec(sql.slice(index));
|
|
155
|
+
const text = match[0];
|
|
156
|
+
push('number', text, start, start + text.length);
|
|
157
|
+
index += text.length;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (char === '`' || /[A-Za-z_]/.test(char)) {
|
|
161
|
+
const segments = [];
|
|
162
|
+
let bareOnly = true;
|
|
163
|
+
while (true) {
|
|
164
|
+
if (sql[index] === '`') {
|
|
165
|
+
bareOnly = false;
|
|
166
|
+
index += 1;
|
|
167
|
+
let segment = '';
|
|
168
|
+
let closed = false;
|
|
169
|
+
while (index < sql.length) {
|
|
170
|
+
if (sql[index] === '`') {
|
|
171
|
+
if (sql[index + 1] === '`') {
|
|
172
|
+
segment += '`';
|
|
173
|
+
index += 2;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
closed = true;
|
|
177
|
+
index += 1;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
segment += sql[index];
|
|
181
|
+
index += 1;
|
|
182
|
+
}
|
|
183
|
+
if (!closed) {
|
|
184
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Unterminated quoted identifier.', start, sql.length);
|
|
185
|
+
}
|
|
186
|
+
if (!BARE_IDENTIFIER_FULL.test(segment)) {
|
|
187
|
+
fail('HQ_SQL_PORT_SYNTAX', `Quoted identifier "${segment}" is not portable.`, start, index);
|
|
188
|
+
}
|
|
189
|
+
segments.push(segment);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
const bare = BARE_IDENTIFIER.exec(sql.slice(index));
|
|
193
|
+
if (!bare)
|
|
194
|
+
break;
|
|
195
|
+
segments.push(bare[0]);
|
|
196
|
+
index += bare[0].length;
|
|
197
|
+
}
|
|
198
|
+
if (sql[index] === '.') {
|
|
199
|
+
if (sql[index + 1] === '`' || /[A-Za-z_]/.test(sql[index + 1] ?? '')) {
|
|
200
|
+
index += 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Expected an identifier after ".".', index, index + 1);
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
const value = segments.join('.');
|
|
208
|
+
if (bareOnly && segments.length === 1) {
|
|
209
|
+
const keyword = KEYWORDS[value.toUpperCase()];
|
|
210
|
+
if (keyword !== undefined) {
|
|
211
|
+
push('keyword', keyword, start, index);
|
|
212
|
+
}
|
|
213
|
+
else if (UNSUPPORTED_KEYWORDS.has(value.toUpperCase())) {
|
|
214
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_SYNTAX', `"${value}" is not part of the portable expression subset.`, start, index);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
push('identifier', value, start, index);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
push('identifier', value, start, index);
|
|
222
|
+
}
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_SYNTAX', `Unexpected character "${char}".`, start, start + 1);
|
|
226
|
+
}
|
|
227
|
+
return tokens;
|
|
228
|
+
}
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Parser
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
const CALL_ARITY = Object.freeze({
|
|
233
|
+
nullifzero: [1, 1],
|
|
234
|
+
coalesce: [2, 2],
|
|
235
|
+
round: [1, 2],
|
|
236
|
+
floor: [1, 1],
|
|
237
|
+
ceil: [1, 1],
|
|
238
|
+
});
|
|
239
|
+
const CALL_CANONICAL = Object.freeze({
|
|
240
|
+
nullifzero: 'nullIfZero',
|
|
241
|
+
coalesce: 'coalesce',
|
|
242
|
+
round: 'round',
|
|
243
|
+
floor: 'floor',
|
|
244
|
+
ceil: 'ceil',
|
|
245
|
+
});
|
|
246
|
+
const BINARY_OPERATORS = Object.freeze({
|
|
247
|
+
'+': 'add',
|
|
248
|
+
'-': 'subtract',
|
|
249
|
+
'*': 'multiply',
|
|
250
|
+
'/': 'divide',
|
|
251
|
+
});
|
|
252
|
+
const COMPARISON_OPERATORS = Object.freeze({
|
|
253
|
+
'=': 'eq',
|
|
254
|
+
'!=': 'neq',
|
|
255
|
+
'>': 'gt',
|
|
256
|
+
'>=': 'gte',
|
|
257
|
+
'<': 'lt',
|
|
258
|
+
'<=': 'lte',
|
|
259
|
+
});
|
|
260
|
+
function node(expression) {
|
|
261
|
+
return Object.freeze(expression);
|
|
262
|
+
}
|
|
263
|
+
class Parser {
|
|
264
|
+
tokens;
|
|
265
|
+
limits;
|
|
266
|
+
position = 0;
|
|
267
|
+
nodes = 0;
|
|
268
|
+
dependencies = new Set();
|
|
269
|
+
constructor(tokens, limits) {
|
|
270
|
+
this.tokens = tokens;
|
|
271
|
+
this.limits = limits;
|
|
272
|
+
}
|
|
273
|
+
parse() {
|
|
274
|
+
if (this.tokens.length === 0)
|
|
275
|
+
fail('HQ_SQL_PORT_SYNTAX', 'The expression is empty.', 0, 0);
|
|
276
|
+
const expression = this.parseOr(0);
|
|
277
|
+
const rest = this.peek();
|
|
278
|
+
if (rest)
|
|
279
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Unexpected trailing input.', rest.start, rest.end);
|
|
280
|
+
return {
|
|
281
|
+
expression,
|
|
282
|
+
dependencies: Object.freeze([...this.dependencies].sort()),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
peek() {
|
|
286
|
+
return this.tokens[this.position];
|
|
287
|
+
}
|
|
288
|
+
next() {
|
|
289
|
+
const token = this.tokens[this.position];
|
|
290
|
+
if (!token) {
|
|
291
|
+
const end = this.tokens.length === 0 ? 0 : this.tokens[this.tokens.length - 1].end;
|
|
292
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Unexpected end of expression.', end, end);
|
|
293
|
+
}
|
|
294
|
+
this.position += 1;
|
|
295
|
+
return token;
|
|
296
|
+
}
|
|
297
|
+
enter(depth, token) {
|
|
298
|
+
this.nodes += 1;
|
|
299
|
+
if (this.nodes > this.limits.maxNodes) {
|
|
300
|
+
fail('HQ_SQL_PORT_TOO_COMPLEX', 'The expression exceeds its node limit.', token.start, token.end);
|
|
301
|
+
}
|
|
302
|
+
if (depth > this.limits.maxDepth) {
|
|
303
|
+
fail('HQ_SQL_PORT_TOO_COMPLEX', 'The expression exceeds its depth limit.', token.start, token.end);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
parseOr(depth) {
|
|
307
|
+
const operands = [this.parseAnd(depth)];
|
|
308
|
+
while (this.peek()?.type === 'keyword' && this.peek().value === 'or') {
|
|
309
|
+
this.next();
|
|
310
|
+
operands.push(this.parseAnd(depth));
|
|
311
|
+
}
|
|
312
|
+
if (operands.length === 1)
|
|
313
|
+
return operands[0];
|
|
314
|
+
this.enter(depth, this.tokens[this.position - 1]);
|
|
315
|
+
return node({ kind: 'logical', operator: 'or', operands: Object.freeze(operands) });
|
|
316
|
+
}
|
|
317
|
+
parseAnd(depth) {
|
|
318
|
+
const operands = [this.parseNot(depth)];
|
|
319
|
+
while (this.peek()?.type === 'keyword' && this.peek().value === 'and') {
|
|
320
|
+
this.next();
|
|
321
|
+
operands.push(this.parseNot(depth));
|
|
322
|
+
}
|
|
323
|
+
if (operands.length === 1)
|
|
324
|
+
return operands[0];
|
|
325
|
+
this.enter(depth, this.tokens[this.position - 1]);
|
|
326
|
+
return node({ kind: 'logical', operator: 'and', operands: Object.freeze(operands) });
|
|
327
|
+
}
|
|
328
|
+
parseNot(depth) {
|
|
329
|
+
const token = this.peek();
|
|
330
|
+
if (token?.type === 'keyword' && token.value === 'not') {
|
|
331
|
+
this.next();
|
|
332
|
+
this.enter(depth + 1, token);
|
|
333
|
+
return node({
|
|
334
|
+
kind: 'logical',
|
|
335
|
+
operator: 'not',
|
|
336
|
+
operand: this.parseNot(depth + 1),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return this.parseComparison(depth);
|
|
340
|
+
}
|
|
341
|
+
parseComparison(depth) {
|
|
342
|
+
const left = this.parseAdditive(depth);
|
|
343
|
+
const token = this.peek();
|
|
344
|
+
if (!token)
|
|
345
|
+
return left;
|
|
346
|
+
if (token.type === 'operator' && COMPARISON_OPERATORS[token.value] !== undefined) {
|
|
347
|
+
this.next();
|
|
348
|
+
const operator = COMPARISON_OPERATORS[token.value];
|
|
349
|
+
const right = this.parseAdditive(depth);
|
|
350
|
+
this.enter(depth + 1, token);
|
|
351
|
+
this.rejectChained();
|
|
352
|
+
return node({ kind: 'comparison', operator, left, right });
|
|
353
|
+
}
|
|
354
|
+
if (token.type === 'keyword' && token.value === 'like') {
|
|
355
|
+
this.next();
|
|
356
|
+
const right = this.parseAdditive(depth);
|
|
357
|
+
this.enter(depth + 1, token);
|
|
358
|
+
this.rejectChained();
|
|
359
|
+
return node({ kind: 'comparison', operator: 'like', left, right });
|
|
360
|
+
}
|
|
361
|
+
if (token.type === 'keyword' && token.value === 'between') {
|
|
362
|
+
this.next();
|
|
363
|
+
const lower = this.parseAdditive(depth);
|
|
364
|
+
const junction = this.peek();
|
|
365
|
+
if (junction?.type !== 'keyword' || junction.value !== 'and') {
|
|
366
|
+
fail('HQ_SQL_PORT_SYNTAX', 'BETWEEN requires an AND separator.', junction?.start ?? token.end, junction?.end ?? token.end);
|
|
367
|
+
}
|
|
368
|
+
this.next();
|
|
369
|
+
const upper = this.parseAdditive(depth);
|
|
370
|
+
this.enter(depth + 1, token);
|
|
371
|
+
this.rejectChained();
|
|
372
|
+
return node({
|
|
373
|
+
kind: 'comparison',
|
|
374
|
+
operator: 'between',
|
|
375
|
+
left,
|
|
376
|
+
right: this.tupleLiteral(lower, upper, token),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (token.type === 'keyword' && (token.value === 'in' || token.value === 'not')) {
|
|
380
|
+
this.next();
|
|
381
|
+
if (token.value === 'not') {
|
|
382
|
+
const following = this.peek();
|
|
383
|
+
if (following?.type === 'keyword' && following.value === 'in') {
|
|
384
|
+
this.next();
|
|
385
|
+
return this.parseInList(left, 'notIn', token, depth);
|
|
386
|
+
}
|
|
387
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_OPERATOR', 'Only NOT IN is portable; NOT LIKE and NOT BETWEEN are not.', token.start, (following ?? token).end);
|
|
388
|
+
}
|
|
389
|
+
return this.parseInList(left, 'in', token, depth);
|
|
390
|
+
}
|
|
391
|
+
return left;
|
|
392
|
+
}
|
|
393
|
+
rejectChained() {
|
|
394
|
+
const token = this.peek();
|
|
395
|
+
if (token && (token.type === 'operator' && COMPARISON_OPERATORS[token.value] !== undefined
|
|
396
|
+
|| token.type === 'keyword'
|
|
397
|
+
&& ['like', 'between', 'in'].includes(token.value))) {
|
|
398
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Chained comparisons are not portable; combine them with AND/OR.', token.start, token.end);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
tupleLiteral(lower, upper, token) {
|
|
402
|
+
for (const bound of [lower, upper]) {
|
|
403
|
+
if (bound.kind !== 'literal') {
|
|
404
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_SYNTAX', 'BETWEEN bounds must be literal values.', token.start, token.end);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const bounds = [lower, upper];
|
|
408
|
+
return node({
|
|
409
|
+
kind: 'literal',
|
|
410
|
+
value: {
|
|
411
|
+
$hypequery: {
|
|
412
|
+
type: 'tuple',
|
|
413
|
+
version: 1,
|
|
414
|
+
values: [bounds[0].value, bounds[1].value],
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
parseInList(left, operator, token, depth) {
|
|
420
|
+
const open = this.peek();
|
|
421
|
+
if (open?.type !== 'operator' || open.value !== '(') {
|
|
422
|
+
fail('HQ_SQL_PORT_SYNTAX', 'IN requires a parenthesized literal list.', token.start, token.end);
|
|
423
|
+
}
|
|
424
|
+
this.next();
|
|
425
|
+
const values = [];
|
|
426
|
+
if (this.peek()?.type === 'operator' && this.peek().value === ')') {
|
|
427
|
+
fail('HQ_SQL_PORT_SYNTAX', 'IN requires at least one element.', open.start, open.end);
|
|
428
|
+
}
|
|
429
|
+
while (true) {
|
|
430
|
+
const element = this.parseUnary(depth + 1);
|
|
431
|
+
if (element.kind !== 'literal') {
|
|
432
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_SYNTAX', 'IN list elements must be literal values.', token.start, token.end);
|
|
433
|
+
}
|
|
434
|
+
values.push(element.value);
|
|
435
|
+
const separator = this.peek();
|
|
436
|
+
if (separator?.type === 'operator' && separator.value === ',') {
|
|
437
|
+
this.next();
|
|
438
|
+
const following = this.peek();
|
|
439
|
+
if (following?.type === 'operator' && following.value === ')') {
|
|
440
|
+
fail('HQ_SQL_PORT_SYNTAX', 'IN lists do not allow a trailing comma.', separator.start, following.end);
|
|
441
|
+
}
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (separator?.type === 'operator' && separator.value === ')') {
|
|
445
|
+
this.next();
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Expected "," or ")" in the IN list.', separator?.start ?? token.end, separator?.end ?? token.end);
|
|
449
|
+
}
|
|
450
|
+
this.enter(depth + 1, token);
|
|
451
|
+
this.rejectChained();
|
|
452
|
+
return node({
|
|
453
|
+
kind: 'comparison',
|
|
454
|
+
operator,
|
|
455
|
+
left,
|
|
456
|
+
right: {
|
|
457
|
+
kind: 'literal',
|
|
458
|
+
value: {
|
|
459
|
+
$hypequery: {
|
|
460
|
+
type: 'array',
|
|
461
|
+
version: 1,
|
|
462
|
+
values,
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
parseAdditive(depth) {
|
|
469
|
+
let left = this.parseMultiplicative(depth);
|
|
470
|
+
while (true) {
|
|
471
|
+
const token = this.peek();
|
|
472
|
+
if (token?.type !== 'operator' || (token.value !== '+' && token.value !== '-'))
|
|
473
|
+
break;
|
|
474
|
+
this.next();
|
|
475
|
+
const right = this.parseMultiplicative(depth + 1);
|
|
476
|
+
this.enter(depth + 1, token);
|
|
477
|
+
left = node({
|
|
478
|
+
kind: 'binary',
|
|
479
|
+
operator: BINARY_OPERATORS[token.value],
|
|
480
|
+
left,
|
|
481
|
+
right,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return left;
|
|
485
|
+
}
|
|
486
|
+
parseMultiplicative(depth) {
|
|
487
|
+
let left = this.parseUnary(depth);
|
|
488
|
+
while (true) {
|
|
489
|
+
const token = this.peek();
|
|
490
|
+
if (token?.type !== 'operator' || (token.value !== '*' && token.value !== '/'))
|
|
491
|
+
break;
|
|
492
|
+
this.next();
|
|
493
|
+
const right = this.parseUnary(depth + 1);
|
|
494
|
+
this.enter(depth + 1, token);
|
|
495
|
+
left = node({
|
|
496
|
+
kind: 'binary',
|
|
497
|
+
operator: BINARY_OPERATORS[token.value],
|
|
498
|
+
left,
|
|
499
|
+
right,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return left;
|
|
503
|
+
}
|
|
504
|
+
parseUnary(depth) {
|
|
505
|
+
let token = this.peek();
|
|
506
|
+
while (token?.type === 'operator' && token.value === '+') {
|
|
507
|
+
this.next();
|
|
508
|
+
token = this.peek();
|
|
509
|
+
}
|
|
510
|
+
if (token?.type === 'operator' && token.value === '-') {
|
|
511
|
+
this.next();
|
|
512
|
+
const operand = this.peek();
|
|
513
|
+
if (operand?.type !== 'number') {
|
|
514
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_OPERATOR', 'Negation is only portable on numeric literals.', token.start, token.end);
|
|
515
|
+
}
|
|
516
|
+
this.next();
|
|
517
|
+
return this.numericLiteral(operand, true);
|
|
518
|
+
}
|
|
519
|
+
return this.parsePrimary(depth);
|
|
520
|
+
}
|
|
521
|
+
parsePrimary(depth) {
|
|
522
|
+
const token = this.next();
|
|
523
|
+
this.enter(depth, token);
|
|
524
|
+
switch (token.type) {
|
|
525
|
+
case 'number':
|
|
526
|
+
return this.numericLiteral(token, false);
|
|
527
|
+
case 'string':
|
|
528
|
+
return node({ kind: 'literal', value: token.value });
|
|
529
|
+
case 'keyword':
|
|
530
|
+
if (token.value === 'true')
|
|
531
|
+
return node({ kind: 'literal', value: true });
|
|
532
|
+
if (token.value === 'false')
|
|
533
|
+
return node({ kind: 'literal', value: false });
|
|
534
|
+
if (token.value === 'null')
|
|
535
|
+
return node({ kind: 'literal', value: null });
|
|
536
|
+
break;
|
|
537
|
+
case 'identifier': {
|
|
538
|
+
const open = this.peek();
|
|
539
|
+
if (open?.type === 'operator' && open.value === '(') {
|
|
540
|
+
return this.parseCall(token, depth);
|
|
541
|
+
}
|
|
542
|
+
let name;
|
|
543
|
+
try {
|
|
544
|
+
name = parseProtocolQualifiedIdentifier(token.value);
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
if (error instanceof ProtocolIdentifierError) {
|
|
548
|
+
fail('HQ_SQL_PORT_SYNTAX', `Identifier "${token.value}" is not portable.`, token.start, token.end);
|
|
549
|
+
}
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
this.dependencies.add(token.value);
|
|
553
|
+
return node({ kind: 'reference', name });
|
|
554
|
+
}
|
|
555
|
+
case 'operator':
|
|
556
|
+
if (token.value === '(') {
|
|
557
|
+
const expression = this.parseOr(depth + 1);
|
|
558
|
+
const close = this.peek();
|
|
559
|
+
if (close?.type !== 'operator' || close.value !== ')') {
|
|
560
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Expected ")".', close?.start ?? token.end, close?.end ?? token.end);
|
|
561
|
+
}
|
|
562
|
+
this.next();
|
|
563
|
+
return expression;
|
|
564
|
+
}
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
fail('HQ_SQL_PORT_SYNTAX', `Unexpected "${token.value}".`, token.start, token.end);
|
|
568
|
+
}
|
|
569
|
+
parseCall(name, depth) {
|
|
570
|
+
const key = name.value.toLowerCase();
|
|
571
|
+
const canonical = CALL_CANONICAL[key];
|
|
572
|
+
const arity = CALL_ARITY[key];
|
|
573
|
+
if (canonical === undefined || arity === undefined) {
|
|
574
|
+
const open = this.peek();
|
|
575
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_FUNCTION', `Function "${name.value}" is not in the portable allowlist.`, name.start, open.end);
|
|
576
|
+
}
|
|
577
|
+
this.next(); // consume '('
|
|
578
|
+
const args = [];
|
|
579
|
+
if (!(this.peek()?.type === 'operator' && this.peek().value === ')')) {
|
|
580
|
+
while (true) {
|
|
581
|
+
args.push(this.parseOr(depth + 1));
|
|
582
|
+
const separator = this.peek();
|
|
583
|
+
if (separator?.type === 'operator' && separator.value === ',') {
|
|
584
|
+
this.next();
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const close = this.peek();
|
|
591
|
+
if (close?.type !== 'operator' || close.value !== ')') {
|
|
592
|
+
fail('HQ_SQL_PORT_SYNTAX', 'Expected ")".', close?.start ?? name.end, close?.end ?? name.end);
|
|
593
|
+
}
|
|
594
|
+
this.next();
|
|
595
|
+
if (args.length < arity[0] || args.length > arity[1]) {
|
|
596
|
+
fail('HQ_SQL_PORT_SYNTAX', `Function "${canonical}" expects ${arity[0] === arity[1] ? arity[0] : `${arity[0]} to ${arity[1]}`} arguments.`, name.start, close.end);
|
|
597
|
+
}
|
|
598
|
+
return node({
|
|
599
|
+
kind: 'call',
|
|
600
|
+
function: canonical,
|
|
601
|
+
args: Object.freeze(args),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
numericLiteral(token, negated) {
|
|
605
|
+
const value = Number(token.value) * (negated ? -1 : 1);
|
|
606
|
+
if (!Number.isFinite(value)
|
|
607
|
+
|| Object.is(value, -0)
|
|
608
|
+
|| (Number.isInteger(value) && !Number.isSafeInteger(value))) {
|
|
609
|
+
fail('HQ_SQL_PORT_UNSUPPORTED_LITERAL', `Numeric literal "${token.value}" is outside the portable range.`, token.start, token.end);
|
|
610
|
+
}
|
|
611
|
+
return node({ kind: 'literal', value });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const textEncoder = new TextEncoder();
|
|
615
|
+
/**
|
|
616
|
+
* Compile a SQL expression fragment into an RFC 0003 expression. The result
|
|
617
|
+
* expression always passes RFC 0003 validation; dependencies are the sorted,
|
|
618
|
+
* deduplicated referenced identifiers.
|
|
619
|
+
*/
|
|
620
|
+
export function compilePortableSqlExpression(sql, options = {}) {
|
|
621
|
+
const limits = resolveSqlPortabilityLimits(options);
|
|
622
|
+
try {
|
|
623
|
+
if (typeof sql !== 'string' || sql.length > limits.maxInputBytes
|
|
624
|
+
|| textEncoder.encode(sql).byteLength > limits.maxInputBytes) {
|
|
625
|
+
fail('HQ_SQL_PORT_TOO_LARGE', 'The expression exceeds its byte limit.', 0, typeof sql === 'string' ? sql.length : 0);
|
|
626
|
+
}
|
|
627
|
+
const parsed = new Parser(tokenize(sql), limits).parse();
|
|
628
|
+
const expression = validateProtocolExpression(parsed.expression);
|
|
629
|
+
return Object.freeze({
|
|
630
|
+
portable: true,
|
|
631
|
+
expression,
|
|
632
|
+
dependencies: parsed.dependencies,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
if (error instanceof SqlPortabilityError) {
|
|
637
|
+
return Object.freeze({ portable: false, issues: Object.freeze([error.issue]) });
|
|
638
|
+
}
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
}
|