@hypequery/clickhouse 2.4.0 → 2.5.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.
Files changed (42) hide show
  1. package/dist/core/compiled/compile.d.ts +25 -0
  2. package/dist/core/compiled/compile.d.ts.map +1 -0
  3. package/dist/core/compiled/compile.js +134 -0
  4. package/dist/core/compiled/debug.d.ts +7 -0
  5. package/dist/core/compiled/debug.d.ts.map +1 -0
  6. package/dist/core/compiled/debug.js +25 -0
  7. package/dist/core/compiled/errors.d.ts +38 -0
  8. package/dist/core/compiled/errors.d.ts.map +1 -0
  9. package/dist/core/compiled/errors.js +91 -0
  10. package/dist/core/compiled/index.d.ts +15 -0
  11. package/dist/core/compiled/index.d.ts.map +1 -0
  12. package/dist/core/compiled/index.js +14 -0
  13. package/dist/core/compiled/parameters.d.ts +28 -0
  14. package/dist/core/compiled/parameters.d.ts.map +1 -0
  15. package/dist/core/compiled/parameters.js +282 -0
  16. package/dist/core/compiled/settings.d.ts +49 -0
  17. package/dist/core/compiled/settings.d.ts.map +1 -0
  18. package/dist/core/compiled/settings.js +65 -0
  19. package/dist/core/compiled/types.d.ts +115 -0
  20. package/dist/core/compiled/types.d.ts.map +1 -0
  21. package/dist/core/compiled/types.js +13 -0
  22. package/dist/core/features/analytics.d.ts.map +1 -1
  23. package/dist/core/features/analytics.js +2 -1
  24. package/dist/core/formatters/sql-formatter.d.ts.map +1 -1
  25. package/dist/core/formatters/sql-formatter.js +19 -6
  26. package/dist/core/query-builder.d.ts.map +1 -1
  27. package/dist/core/query-builder.js +2 -0
  28. package/dist/core/utils/predicate-builder.d.ts.map +1 -1
  29. package/dist/core/utils/predicate-builder.js +12 -2
  30. package/dist/core/utils/sql-parens.d.ts +25 -0
  31. package/dist/core/utils/sql-parens.d.ts.map +1 -0
  32. package/dist/core/utils/sql-parens.js +212 -0
  33. package/dist/core/utils.d.ts +2 -2
  34. package/dist/core/utils.d.ts.map +1 -1
  35. package/dist/core/utils.js +20 -6
  36. package/dist/dataset/sql-tag.d.ts +5 -0
  37. package/dist/dataset/sql-tag.d.ts.map +1 -1
  38. package/dist/dataset/sql-tag.js +7 -2
  39. package/dist/index.d.ts +1 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +5 -0
  42. package/package.json +4 -3
@@ -0,0 +1,282 @@
1
+ import { ProtocolValueError, isProtocolIdentifier, validateCanonicalValue, } from '@hypequery/protocol';
2
+ import { CompiledQueryError } from './errors.js';
3
+ const LOGICAL_TYPES = new Set([
4
+ 'array', 'boolean', 'bytes', 'date', 'datetime', 'decimal', 'enum', 'float',
5
+ 'integer', 'map', 'null', 'string', 'tuple', 'uuid',
6
+ ]);
7
+ function skipQuoted(sql, start, quote) {
8
+ let index = start + 1;
9
+ while (index < sql.length) {
10
+ if (sql[index] === '\\') {
11
+ index += 2;
12
+ continue;
13
+ }
14
+ if (sql[index] === quote) {
15
+ if (sql[index + 1] === quote) {
16
+ index += 2;
17
+ continue;
18
+ }
19
+ return index + 1;
20
+ }
21
+ index += 1;
22
+ }
23
+ return sql.length;
24
+ }
25
+ function parsePlaceholder(sql, start) {
26
+ let index = start + 1;
27
+ while (/\s/.test(sql[index] ?? ''))
28
+ index += 1;
29
+ const nameStart = index;
30
+ if (!/[A-Za-z_]/.test(sql[index] ?? ''))
31
+ return undefined;
32
+ index += 1;
33
+ while (/[A-Za-z0-9_]/.test(sql[index] ?? ''))
34
+ index += 1;
35
+ const name = sql.slice(nameStart, index);
36
+ while (/\s/.test(sql[index] ?? ''))
37
+ index += 1;
38
+ if (sql[index] !== ':')
39
+ return undefined;
40
+ index += 1;
41
+ const typeStart = index;
42
+ let quote;
43
+ let parenthesisDepth = 0;
44
+ while (index < sql.length) {
45
+ const character = sql[index];
46
+ if (quote) {
47
+ if (character === '\\') {
48
+ index += 2;
49
+ continue;
50
+ }
51
+ if (character === quote) {
52
+ if (sql[index + 1] === quote) {
53
+ index += 2;
54
+ continue;
55
+ }
56
+ quote = undefined;
57
+ }
58
+ index += 1;
59
+ continue;
60
+ }
61
+ if (character === "'" || character === '"' || character === '`') {
62
+ quote = character;
63
+ index += 1;
64
+ continue;
65
+ }
66
+ if (character === '(')
67
+ parenthesisDepth += 1;
68
+ if (character === ')') {
69
+ parenthesisDepth -= 1;
70
+ if (parenthesisDepth < 0)
71
+ return undefined;
72
+ }
73
+ if (character === '{')
74
+ return undefined;
75
+ if (character === '}' && parenthesisDepth === 0) {
76
+ const type = sql.slice(typeStart, index).trim();
77
+ if (type.length === 0)
78
+ return undefined;
79
+ return { name, type, start, end: index + 1 };
80
+ }
81
+ index += 1;
82
+ }
83
+ return undefined;
84
+ }
85
+ function scanSqlParameters(sql) {
86
+ const references = [];
87
+ let hasPositionalPlaceholder = false;
88
+ let index = 0;
89
+ while (index < sql.length) {
90
+ const character = sql[index];
91
+ if (character === "'" || character === '"' || character === '`') {
92
+ index = skipQuoted(sql, index, character);
93
+ continue;
94
+ }
95
+ if (character === '-' && sql[index + 1] === '-') {
96
+ const newline = sql.indexOf('\n', index + 2);
97
+ index = newline === -1 ? sql.length : newline + 1;
98
+ continue;
99
+ }
100
+ if (character === '#') {
101
+ const newline = sql.indexOf('\n', index + 1);
102
+ index = newline === -1 ? sql.length : newline + 1;
103
+ continue;
104
+ }
105
+ if (character === '/' && sql[index + 1] === '*') {
106
+ const close = sql.indexOf('*/', index + 2);
107
+ index = close === -1 ? sql.length : close + 2;
108
+ continue;
109
+ }
110
+ if (character === '?')
111
+ hasPositionalPlaceholder = true;
112
+ if (character === '{') {
113
+ const reference = parsePlaceholder(sql, index);
114
+ if (reference) {
115
+ references.push(reference);
116
+ index = reference.end;
117
+ continue;
118
+ }
119
+ }
120
+ index += 1;
121
+ }
122
+ return { references, hasPositionalPlaceholder };
123
+ }
124
+ /** Extract the set of parameter names a SQL text references via `{name:Type}`. */
125
+ export function extractReferencedParameters(sql) {
126
+ return new Set(scanSqlParameters(sql).references.map(({ name }) => name));
127
+ }
128
+ export function replaceParameterPlaceholders(sql, replace) {
129
+ const references = scanSqlParameters(sql).references;
130
+ let result = '';
131
+ let offset = 0;
132
+ for (const reference of references) {
133
+ result += sql.slice(offset, reference.start);
134
+ result += replace(reference.name, reference.type);
135
+ offset = reference.end;
136
+ }
137
+ return result + sql.slice(offset);
138
+ }
139
+ /**
140
+ * Validate that the SQL only references declared parameters. Fails closed when a
141
+ * placeholder names an undeclared parameter (RFC 0010 §Parameters).
142
+ */
143
+ export function validateParameterReferences(sql, declarations) {
144
+ const declared = new Map(declarations.map((declaration) => [
145
+ declaration.name,
146
+ declaration,
147
+ ]));
148
+ for (const reference of scanSqlParameters(sql).references) {
149
+ const declaration = declared.get(reference.name);
150
+ if (!declaration) {
151
+ throw new CompiledQueryError('input-invalid', `SQL references undeclared parameter ${reference.name}.`);
152
+ }
153
+ if (reference.type !== declaration.type.clickHouseType) {
154
+ throw new CompiledQueryError('input-invalid', `SQL parameter ${reference.name} does not match its declared ClickHouse type.`);
155
+ }
156
+ }
157
+ }
158
+ /**
159
+ * Resolve supplied values against declarations into the native `{name: value}` bindings
160
+ * an adapter binds to server parameters. Fail-closed rules (RFC 0010 §Parameters):
161
+ * - a supplied name that is not declared is rejected;
162
+ * - a required declared name with no supplied value is rejected;
163
+ * - an optional declared name may be absent;
164
+ * - every supplied value is validated (RFC 0001 for tagged values).
165
+ *
166
+ * No value is ever concatenated into SQL text: values live only in the returned bindings.
167
+ */
168
+ export function buildParameterBindings(declarations, values) {
169
+ const declaredByName = new Map();
170
+ for (const declaration of declarations) {
171
+ if (!isProtocolIdentifier(declaration.name)) {
172
+ throw new CompiledQueryError('input-invalid', 'Parameter declaration has an invalid name.');
173
+ }
174
+ if (declaredByName.has(declaration.name)) {
175
+ throw new CompiledQueryError('input-invalid', `Duplicate parameter declaration ${declaration.name}.`);
176
+ }
177
+ declaredByName.set(declaration.name, declaration);
178
+ }
179
+ for (const suppliedName of Object.keys(values)) {
180
+ if (!declaredByName.has(suppliedName)) {
181
+ throw new CompiledQueryError('input-invalid', `Value supplied for undeclared parameter ${suppliedName}.`);
182
+ }
183
+ }
184
+ const bindings = {};
185
+ for (const [name, declaration] of declaredByName) {
186
+ const present = Object.prototype.hasOwnProperty.call(values, name);
187
+ if (!present) {
188
+ if (declaration.optional)
189
+ continue;
190
+ throw new CompiledQueryError('input-invalid', `Required parameter ${name} is missing.`);
191
+ }
192
+ bindings[name] = validateParameterValue(name, declaration, values[name]);
193
+ }
194
+ return Object.freeze(bindings);
195
+ }
196
+ function validateParameterValue(name, declaration, value) {
197
+ try {
198
+ const validated = validateCanonicalValue(value, {
199
+ declaredClickHouseType: declaration.type.clickHouseType,
200
+ });
201
+ validateLogicalType(name, declaration, validated);
202
+ return validated;
203
+ }
204
+ catch (error) {
205
+ if (error instanceof ProtocolValueError) {
206
+ throw new CompiledQueryError('input-invalid', `Parameter ${name} failed value validation (${error.code}).`, { cause: error });
207
+ }
208
+ throw error;
209
+ }
210
+ }
211
+ function logicalTypeOf(value) {
212
+ if (value === null)
213
+ return 'null';
214
+ if (typeof value === 'string')
215
+ return 'string';
216
+ if (typeof value === 'number')
217
+ return 'float';
218
+ if (typeof value === 'boolean')
219
+ return 'boolean';
220
+ return value.$hypequery.type;
221
+ }
222
+ function unwrapNullable(type) {
223
+ const trimmed = type.trim();
224
+ if (trimmed.startsWith('Nullable(') && trimmed.endsWith(')')) {
225
+ return { nullable: true, inner: trimmed.slice(9, -1).trim() };
226
+ }
227
+ return { nullable: false, inner: trimmed };
228
+ }
229
+ function clickHouseTypeSupportsLogical(type, logical) {
230
+ const { nullable, inner } = unwrapNullable(type);
231
+ if (logical === 'null')
232
+ return nullable;
233
+ switch (logical) {
234
+ case 'string': return /^(?:String|FixedString\(\d+\)|LowCardinality\(String\))$/.test(inner);
235
+ case 'float': return /^Float(?:32|64)$/.test(inner);
236
+ case 'boolean': return /^(?:Bool|Boolean)$/.test(inner);
237
+ case 'integer': return /^(?:U?Int)(?:8|16|32|64|128|256)$/.test(inner);
238
+ case 'decimal': return /^Decimal(?:(?:32|64|128|256)\(\d+\)|\(\d+\s*,\s*\d+\))$/.test(inner);
239
+ case 'date': return /^(?:Date|Date32)$/.test(inner);
240
+ case 'datetime': return /^DateTime(?:64)?(?:\(.*\))?$/.test(inner);
241
+ case 'uuid': return inner === 'UUID';
242
+ case 'bytes': return /^(?:String|FixedString\(\d+\))$/.test(inner);
243
+ case 'enum': return /^Enum(?:8|16)\(.*\)$/.test(inner);
244
+ case 'array': return /^Array\(.+\)$/.test(inner);
245
+ case 'tuple': return /^Tuple\(.+\)$/.test(inner);
246
+ case 'map': return /^Map\(.+\)$/.test(inner);
247
+ default: return false;
248
+ }
249
+ }
250
+ function validateLogicalType(name, declaration, value) {
251
+ const actual = logicalTypeOf(value);
252
+ const declared = declaration.type.logical;
253
+ if (!LOGICAL_TYPES.has(declared) || (actual !== declared && actual !== 'null')) {
254
+ throw new CompiledQueryError('input-invalid', `Parameter ${name} does not match its declared logical type.`);
255
+ }
256
+ if (!clickHouseTypeSupportsLogical(declaration.type.clickHouseType, actual)) {
257
+ throw new CompiledQueryError('input-invalid', `Parameter ${name} does not match its declared ClickHouse type.`);
258
+ }
259
+ }
260
+ /**
261
+ * Assert the invariant that no bound value has leaked into the SQL text. The compile path
262
+ * never substitutes values, so this is a defense-in-depth check: the SQL must reference
263
+ * every non-optional bound name through a placeholder and must not be the legacy
264
+ * positional form (`?`).
265
+ */
266
+ export function assertNoValuesInSql(sql, bindings) {
267
+ const scan = scanSqlParameters(sql);
268
+ if (scan.hasPositionalPlaceholder) {
269
+ throw new CompiledQueryError('input-invalid', 'Compiled SQL must not use positional placeholders.');
270
+ }
271
+ const referenced = new Set(scan.references.map(({ name }) => name));
272
+ for (const name of Object.keys(bindings)) {
273
+ if (!referenced.has(name)) {
274
+ throw new CompiledQueryError('input-invalid', `Bound parameter ${name} is not referenced by the SQL.`);
275
+ }
276
+ }
277
+ for (const name of referenced) {
278
+ if (!Object.prototype.hasOwnProperty.call(bindings, name)) {
279
+ throw new CompiledQueryError('input-invalid', `SQL parameter ${name} does not have a bound value.`);
280
+ }
281
+ }
282
+ }
@@ -0,0 +1,49 @@
1
+ import type { CompiledDeadline, CompiledSettings } from './types.js';
2
+ /**
3
+ * Closed, typed settings allow-list (RFC 0010 §Settings). Each entry defines an inclusive
4
+ * range; products may TIGHTEN a range but never loosen it. Settings originate only from
5
+ * trusted components — a request can never set, override, or relax them.
6
+ */
7
+ export interface SettingBound {
8
+ readonly min: number;
9
+ readonly max: number;
10
+ }
11
+ export declare const COMPILED_SETTING_BOUNDS: Readonly<{
12
+ readonly maxExecutionMs: {
13
+ readonly min: 1;
14
+ readonly max: 3600000;
15
+ };
16
+ readonly maxResultRows: {
17
+ readonly min: 0;
18
+ readonly max: 1000000000000;
19
+ };
20
+ readonly maxResultBytes: {
21
+ readonly min: 0;
22
+ readonly max: 1099511627776;
23
+ };
24
+ }>;
25
+ export type CompiledSettingName = keyof typeof COMPILED_SETTING_BOUNDS;
26
+ /**
27
+ * Clamp trusted settings into the allow-list. A value outside the closed range is
28
+ * rejected fail-closed rather than silently coerced, since settings are trusted input and
29
+ * an out-of-range value signals a policy bug, not caller data.
30
+ */
31
+ export declare function resolveCompiledSettings(input: CompiledSettings): CompiledSettings;
32
+ export interface DeadlineInputs {
33
+ /** Absolute caller-supplied deadline, epoch-millis. */
34
+ readonly callerAtEpochMs?: number;
35
+ /** Policy-derived maximum window from `now`, milliseconds. */
36
+ readonly policyMaxMs?: number;
37
+ /** Current time, epoch-millis. Injected so resolution stays deterministic/testable. */
38
+ readonly nowEpochMs: number;
39
+ }
40
+ /**
41
+ * Resolve the effective deadline (RFC 0010 §Deadline and cancellation precedence).
42
+ *
43
+ * The effective deadline is the EARLIER of the caller-supplied deadline and the
44
+ * policy-derived maximum — a caller can shorten but never extend the window. A supplied
45
+ * deadline at or before `now` fails immediately with `deadline-exceeded` rather than being
46
+ * extended or ignored.
47
+ */
48
+ export declare function resolveCompiledDeadline(inputs: DeadlineInputs): CompiledDeadline | undefined;
49
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../src/core/compiled/settings.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAErE;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;EAI8B,CAAC;AAEnE,MAAM,MAAM,mBAAmB,GAAG,MAAM,OAAO,uBAAuB,CAAC;AAEvE;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,gBAAgB,GAAG,gBAAgB,CAejF;AAED,MAAM,WAAW,cAAc;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,uFAAuF;IACvF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,cAAc,GAAG,gBAAgB,GAAG,SAAS,CAyC5F"}
@@ -0,0 +1,65 @@
1
+ import { CompiledQueryError } from './errors.js';
2
+ export const COMPILED_SETTING_BOUNDS = Object.freeze({
3
+ maxExecutionMs: { min: 1, max: 3_600_000 },
4
+ maxResultRows: { min: 0, max: 1_000_000_000_000 },
5
+ maxResultBytes: { min: 0, max: 1_099_511_627_776 },
6
+ });
7
+ /**
8
+ * Clamp trusted settings into the allow-list. A value outside the closed range is
9
+ * rejected fail-closed rather than silently coerced, since settings are trusted input and
10
+ * an out-of-range value signals a policy bug, not caller data.
11
+ */
12
+ export function resolveCompiledSettings(input) {
13
+ const resolved = {};
14
+ for (const key of Object.keys(COMPILED_SETTING_BOUNDS)) {
15
+ const value = input[key];
16
+ if (value === undefined)
17
+ continue;
18
+ const bound = COMPILED_SETTING_BOUNDS[key];
19
+ if (!Number.isInteger(value) || value < bound.min || value > bound.max) {
20
+ throw new CompiledQueryError('input-invalid', `Setting ${key} is outside its allowed range.`);
21
+ }
22
+ resolved[key] = value;
23
+ }
24
+ return Object.freeze(resolved);
25
+ }
26
+ /**
27
+ * Resolve the effective deadline (RFC 0010 §Deadline and cancellation precedence).
28
+ *
29
+ * The effective deadline is the EARLIER of the caller-supplied deadline and the
30
+ * policy-derived maximum — a caller can shorten but never extend the window. A supplied
31
+ * deadline at or before `now` fails immediately with `deadline-exceeded` rather than being
32
+ * extended or ignored.
33
+ */
34
+ export function resolveCompiledDeadline(inputs) {
35
+ const { callerAtEpochMs, policyMaxMs, nowEpochMs } = inputs;
36
+ if (!Number.isSafeInteger(nowEpochMs) || nowEpochMs < 0) {
37
+ throw new CompiledQueryError('input-invalid', 'The current time is invalid.');
38
+ }
39
+ if (callerAtEpochMs !== undefined
40
+ && (!Number.isSafeInteger(callerAtEpochMs) || callerAtEpochMs < 0)) {
41
+ throw new CompiledQueryError('input-invalid', 'The supplied deadline is invalid.');
42
+ }
43
+ if (policyMaxMs !== undefined && (!Number.isSafeInteger(policyMaxMs) || policyMaxMs <= 0)) {
44
+ throw new CompiledQueryError('input-invalid', 'The policy deadline window is invalid.');
45
+ }
46
+ const policyAt = policyMaxMs === undefined ? undefined : nowEpochMs + policyMaxMs;
47
+ if (policyAt !== undefined && !Number.isSafeInteger(policyAt)) {
48
+ throw new CompiledQueryError('input-invalid', 'The policy deadline is outside the safe range.');
49
+ }
50
+ if (callerAtEpochMs !== undefined && callerAtEpochMs <= nowEpochMs) {
51
+ throw new CompiledQueryError('deadline-exceeded', 'The supplied deadline is at or before the current time.');
52
+ }
53
+ if (callerAtEpochMs === undefined && policyAt === undefined) {
54
+ return undefined;
55
+ }
56
+ if (callerAtEpochMs === undefined) {
57
+ return Object.freeze({ atEpochMs: policyAt, source: 'policy' });
58
+ }
59
+ if (policyAt === undefined) {
60
+ return Object.freeze({ atEpochMs: callerAtEpochMs, source: 'caller' });
61
+ }
62
+ return Object.freeze(callerAtEpochMs <= policyAt
63
+ ? { atEpochMs: callerAtEpochMs, source: 'caller' }
64
+ : { atEpochMs: policyAt, source: 'policy' });
65
+ }
@@ -0,0 +1,115 @@
1
+ import type { ProtocolIdentifier, TaggedValue } from '@hypequery/protocol';
2
+ /**
3
+ * Execution-request contract from RFC 0010 (compiled query, error, cancellation),
4
+ * realized for the ClickHouse runtime.
5
+ *
6
+ * This is the versioned shape a runtime hands to an adapter. It sits *beside* the
7
+ * legacy positional path (`adapter.query(sql, params: unknown[])`, which renders
8
+ * values into SQL text via `substituteParameters`). Nothing here lets a request
9
+ * author SQL, tenant proof, or settings — those are trusted build/policy output.
10
+ *
11
+ * NOTE: distinct from the internal `CompiledQuery` in `../../types/base.ts`, which
12
+ * is the SQL-formatter fragment `{ query, parameters }`.
13
+ */
14
+ export declare const COMPILED_QUERY_VERSION: 1;
15
+ /** Closed operation set (RFC 0010 §Operations). The operation belongs to the compiled
16
+ * query, never to the request. */
17
+ export type CompiledOperation = 'query' | 'command' | 'insert';
18
+ /**
19
+ * A logical parameter type. The logical tag drives validation and the `{name:Type}`
20
+ * placeholder the adapter sends; `clickHouseType` is the concrete server type string
21
+ * (e.g. `UInt64`, `DateTime64(3, 'UTC')`, `Array(String)`).
22
+ */
23
+ export interface CompiledParameterType {
24
+ /** RFC 0001 logical tag, or a scalar shorthand for native JSON scalars. */
25
+ readonly logical: TaggedValue['$hypequery']['type'] | 'boolean' | 'string' | 'float' | 'null';
26
+ /** Concrete ClickHouse server type used to build the native placeholder. */
27
+ readonly clickHouseType: string;
28
+ }
29
+ /**
30
+ * A named, typed parameter declaration carried beside the SQL text. A request supplies
31
+ * only values for declared names; it can neither add names nor change types.
32
+ */
33
+ export interface CompiledParameterDeclaration {
34
+ readonly name: ProtocolIdentifier;
35
+ readonly type: CompiledParameterType;
36
+ /** Optional parameters may be absent from a request; required ones fail closed. */
37
+ readonly optional: boolean;
38
+ }
39
+ /** A supplied parameter value: an RFC 0001 tagged value or a native JSON scalar. */
40
+ export type CompiledParameterValue = TaggedValue | string | number | boolean | null;
41
+ /**
42
+ * The resolved `{name: value}` map the adapter binds to native server parameters.
43
+ * Every key is a declared parameter name; no value is ever rendered into SQL text.
44
+ */
45
+ export type CompiledParameterBindings = Readonly<Record<string, CompiledParameterValue>>;
46
+ /**
47
+ * Data sensitivity metadata. Marks whether the query touches tenant-scoped or otherwise
48
+ * sensitive data so diagnostics/redaction downstream can act without inspecting values.
49
+ */
50
+ export interface CompiledSensitivity {
51
+ /** The compiled SQL already contains trusted tenant predicates (RFC 0010 §Operations). */
52
+ readonly tenantScoped: boolean;
53
+ /** Free-form trusted classification labels (e.g. 'pii'); never request-supplied. */
54
+ readonly labels: readonly string[];
55
+ }
56
+ /**
57
+ * Bounded settings the runtime applies per execution. These originate only from trusted
58
+ * components; a request can never set, override, or relax them. See `settings.ts` for the
59
+ * closed allow-list and ranges.
60
+ */
61
+ export interface CompiledSettings {
62
+ /** Wall-clock ceiling for server-side execution, milliseconds. Always enforced. */
63
+ readonly maxExecutionMs?: number;
64
+ /** Maximum rows the result may contain. */
65
+ readonly maxResultRows?: number;
66
+ /** Maximum bytes the result may contain. */
67
+ readonly maxResultBytes?: number;
68
+ }
69
+ /** Effective deadline + cancellation inputs (RFC 0010 §Deadline and cancellation). */
70
+ export interface CompiledDeadline {
71
+ /** Epoch-millis absolute deadline. Earlier of caller-supplied and policy-derived. */
72
+ readonly atEpochMs: number;
73
+ /** Which side set the effective deadline, for honest diagnostics. */
74
+ readonly source: 'caller' | 'policy';
75
+ }
76
+ /** Authoritative + optional correlation identifiers (RFC 0010 §Query identifier). */
77
+ export interface CompiledIdentifiers {
78
+ /** Server-generated, unique per execution, unguessable, safe for logs/cache metadata. */
79
+ readonly queryId: string;
80
+ /** Caller-supplied, non-authoritative, ≤1024 UTF-8 bytes, no control chars; never
81
+ * influences routing, cache keys, or authorization. */
82
+ readonly correlationId?: string;
83
+ }
84
+ /**
85
+ * Redacted, non-executable debug form (RFC 0010 §Debug form). Shows SQL structure with
86
+ * placeholders and declared types; carries no values, tenant values, credentials, or
87
+ * settings beyond their names. `sql` here is deliberately not valid database SQL.
88
+ */
89
+ export interface CompiledDebugForm {
90
+ readonly sql: string;
91
+ readonly parameters: readonly {
92
+ readonly name: string;
93
+ readonly type: string;
94
+ readonly optional: boolean;
95
+ }[];
96
+ readonly settings: readonly string[];
97
+ }
98
+ /**
99
+ * The compiled query: the only way a runtime asks a ClickHouse adapter to execute.
100
+ */
101
+ export interface CompiledQueryV1 {
102
+ readonly version: typeof COMPILED_QUERY_VERSION;
103
+ readonly operation: CompiledOperation;
104
+ /** Trusted build/server output. Callers influence execution only via `bindings`. */
105
+ readonly sql: string;
106
+ readonly parameters: readonly CompiledParameterDeclaration[];
107
+ /** Resolved values for declared names, bound to native server parameters. */
108
+ readonly bindings: CompiledParameterBindings;
109
+ readonly settings: CompiledSettings;
110
+ readonly identifiers: CompiledIdentifiers;
111
+ readonly deadline?: CompiledDeadline;
112
+ readonly sensitivity: CompiledSensitivity;
113
+ readonly debug: CompiledDebugForm;
114
+ }
115
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/compiled/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Execution-request contract from RFC 0010 (compiled query, error, cancellation),
3
+ * realized for the ClickHouse runtime.
4
+ *
5
+ * This is the versioned shape a runtime hands to an adapter. It sits *beside* the
6
+ * legacy positional path (`adapter.query(sql, params: unknown[])`, which renders
7
+ * values into SQL text via `substituteParameters`). Nothing here lets a request
8
+ * author SQL, tenant proof, or settings — those are trusted build/policy output.
9
+ *
10
+ * NOTE: distinct from the internal `CompiledQuery` in `../../types/base.ts`, which
11
+ * is the SQL-formatter fragment `{ query, parameters }`.
12
+ */
13
+ export const COMPILED_QUERY_VERSION = 1;
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../../src/core/features/analytics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACjG,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D,qBAAa,gBAAgB,CAC3B,MAAM,SAAS,gBAAgB,CAAC,MAAM,CAAC,EACvC,KAAK,SAAS,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC,CAAC,CAAC;IAExF,OAAO,CAAC,OAAO;gBAAP,OAAO,EAAE,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAExD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,CAAC,GAAG,EAAE,eAAe,CAAC,GAAG,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAStH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,mBAAmB,GAAG,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IASnG,eAAe,CACb,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,GAAG,iBAAiB,GAAG,eAAe,GAAG,cAAc,GAAG,eAAe,GAAG,gBAAgB,GAAG,kBAAkB,GAAG,eAAe,EAC9J,OAAO,EAAE,UAAU,GAClB,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAa3C,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAUhF"}
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../../src/core/features/analytics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACjG,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAGzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D,qBAAa,gBAAgB,CAC3B,MAAM,SAAS,gBAAgB,CAAC,MAAM,CAAC,EACvC,KAAK,SAAS,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,MAAM,CAAC,CAAC,CAAC;IAExF,OAAO,CAAC,OAAO;gBAAP,OAAO,EAAE,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAExD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,CAAC,GAAG,EAAE,eAAe,CAAC,GAAG,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAStH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,mBAAmB,GAAG,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAYnG,eAAe,CACb,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,mBAAmB,GAAG,iBAAiB,GAAG,eAAe,GAAG,cAAc,GAAG,eAAe,GAAG,gBAAgB,GAAG,kBAAkB,GAAG,eAAe,EAC9J,OAAO,EAAE,UAAU,GAClB,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAa3C,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAUhF"}
@@ -1,4 +1,5 @@
1
1
  import { substituteParameters } from '../utils.js';
2
+ import { terminateTrailingLineComment } from '../utils/sql-parens.js';
2
3
  export class AnalyticsFeature {
3
4
  builder;
4
5
  constructor(builder) {
@@ -14,7 +15,7 @@ export class AnalyticsFeature {
14
15
  }
15
16
  addScalar(alias, expression) {
16
17
  const query = this.builder.getQueryNode();
17
- const scalarExpression = substituteParameters(expression.sql, expression.parameters);
18
+ const scalarExpression = substituteParameters(terminateTrailingLineComment(expression.sql), expression.parameters);
18
19
  return {
19
20
  ...query,
20
21
  ctes: [...(query.ctes || []), { kind: 'cte', expression: `${scalarExpression} AS ${alias}` }]
@@ -1 +1 @@
1
- {"version":3,"file":"sql-formatter.d.ts","sourceRoot":"","sources":["../../../src/core/formatters/sql-formatter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,KAAK,QAAQ,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAkB,MAAM,sBAAsB,CAAC;AAEhJ,qBAAa,YAAY;IACvB,YAAY,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAMtD,aAAa,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAMvD,gBAAgB,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAO1D,cAAc,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAIxD,WAAW,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAIrD,UAAU,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM;IAUvC,WAAW,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,UAAQ,GAAG,aAAa;IAoD3D,OAAO,CAAC,gBAAgB;IAuFxB,aAAa,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,aAAa;IAY9D,OAAO,CAAC,cAAc;IActB,YAAY,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,aAAa;IAkB7D,WAAW,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAIrD,UAAU,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAKpD,aAAa,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAOvD,aAAa,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAKvD,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,4BAA4B;CAMrC"}
1
+ {"version":3,"file":"sql-formatter.d.ts","sourceRoot":"","sources":["../../../src/core/formatters/sql-formatter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,aAAa,EAAE,KAAK,QAAQ,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAkB,MAAM,sBAAsB,CAAC;AAGhJ,qBAAa,YAAY;IACvB,YAAY,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAMtD,aAAa,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAMvD,gBAAgB,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAO1D,cAAc,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAIxD,WAAW,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAIrD,UAAU,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM;IAUvC,WAAW,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,UAAQ,GAAG,aAAa;IA0D3D,OAAO,CAAC,gBAAgB;IAuFxB,aAAa,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,aAAa;IAkB9D,OAAO,CAAC,cAAc;IActB,YAAY,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,aAAa;IAkB7D,WAAW,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAIrD,UAAU,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAKpD,aAAa,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAOvD,aAAa,CAAC,KAAK,EAAE,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM;IAKvD,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,4BAA4B;CAMrC"}
@@ -1,3 +1,4 @@
1
+ import { hasTopLevelLogicalOperator, terminateTrailingLineComment } from '../utils/sql-parens.js';
1
2
  export class SQLFormatter {
2
3
  formatSelect(query) {
3
4
  const distinctClause = query.distinct ? 'DISTINCT ' : '';
@@ -38,11 +39,17 @@ export class SQLFormatter {
38
39
  if (!expr)
39
40
  return { query: '', parameters: [] };
40
41
  switch (expr.kind) {
41
- case 'raw':
42
+ case 'raw': {
43
+ // A raw fragment with a top-level AND/OR would rebind against sibling
44
+ // conditions when embedded in a sequence (issue #348), so wrap it.
45
+ const rawExpression = terminateTrailingLineComment(expr.expression);
42
46
  return {
43
- query: expr.expression,
47
+ query: nested && hasTopLevelLogicalOperator(rawExpression)
48
+ ? `(${rawExpression})`
49
+ : rawExpression,
44
50
  parameters: expr.parameters.map(parameter => parameter.value),
45
51
  };
52
+ }
46
53
  case 'group': {
47
54
  if (!expr.expression) {
48
55
  return { query: '', parameters: [] };
@@ -173,10 +180,16 @@ export class SQLFormatter {
173
180
  compileHaving(query) {
174
181
  if (!query.having?.length)
175
182
  return { query: '', parameters: [] };
176
- return this.combineCompiledWithSeparator(query.having.map(item => ({
177
- query: item.expression,
178
- parameters: item.parameters?.map(parameter => parameter.value) || [],
179
- })), ' AND ');
183
+ const wrapFragments = query.having.length > 1;
184
+ return this.combineCompiledWithSeparator(query.having.map(item => {
185
+ const expression = terminateTrailingLineComment(item.expression);
186
+ return {
187
+ query: wrapFragments && hasTopLevelLogicalOperator(expression)
188
+ ? `(${expression})`
189
+ : expression,
190
+ parameters: item.parameters?.map(parameter => parameter.value) || [],
191
+ };
192
+ }), ' AND ');
180
193
  }
181
194
  getSqlOperator(operator) {
182
195
  switch (operator) {