@coldsmirk/inkstone-sql 0.32.0 → 0.34.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 CHANGED
@@ -31,7 +31,7 @@ const runnable = runnableStatement(text, from, to, "postgres");
31
31
  if (runnable.kind === "one") {
32
32
  // Markers are read off the BLANKED form — `select '?'` binds nothing — and under the kind's
33
33
  // own binding rules: `markers.refusal` mirrors the engine scanner's own faults (mixed forms,
34
- // `??` on mysql/dm, a hand-written `$1` beside a minted marker), so the editor can say so
34
+ // `??` on mysql/dm/clickhouse, a hand-written `$1` beside a minted marker), so the editor can say so
35
35
  // before the round trip does.
36
36
  const markers = statementMarkers(runnable.blanked, "postgres");
37
37
  }
@@ -41,7 +41,7 @@ Everything the intelligence answers goes through one lexical reader taken under
41
41
 
42
42
  ## The mirror invariant
43
43
 
44
- `noise.ts` (which runs of a statement are strings / comments / quoted names, per kind) and the marker grammar mirror [**polyglot's engine scanner**](https://github.com/coldsmirk/polyglot) (`internal/scan`) flag for flag. The scanner decides what actually binds; a reader that diverged would prompt for a parameter the engine refuses, or send a statement short of one it binds. **A scanner change requires a matching release of this package**, and the per-kind cases in `reader.test.ts` are the invariant's guard.
44
+ `noise.ts` (which runs of a statement are strings / comments / quoted names, per kind) and the marker grammar mirror [**polyglot's engine scanner**](https://github.com/coldsmirk/polyglot) (`sql/Placeholders`, over its profiles in `kinds/Dialect`) flag for flag, each kind under the two lexicons the sidecar reads it by: the database's own says where a statement ends and what is prose, and the one the kind's driver finds a `?` by says where the markers are — SQL Server nests a block comment, but Microsoft's driver ends one at its first close and binds a `?` past it; ClickHouse's driver reads a `?` as the conditional operator `a ? b : c` where a lone `:` follows it before any other `?`, and binds nothing through that `:` (`conditional.ts`). The scanner decides what actually binds; a reader that diverged would prompt for a parameter the engine refuses, or send a statement short of one it binds. **A scanner change requires a matching release of this package**, and the per-kind cases in `reader.test.ts` are the invariant's guard.
45
45
 
46
46
  ## Structured refusals
47
47
 
@@ -14,8 +14,11 @@
14
14
  * Doris (`doris`, StarRocks and SelectDB with it) speaks MySQL's protocol and grammar and reads
15
15
  * here as the MySQL idiom throughout, but its own parser admits no identifier past ASCII bare —
16
16
  * `患者` must be backticked where MySQL takes it as written — so it earned a kind for its spelling.
17
+ * ClickHouse (`clickhouse`) is a lexicon of its own on the engine — backslash escapes and backtick
18
+ * names as MySQL's, but `--` a comment without its space, `?` its own bind marker — and names its
19
+ * functions by their case (`toDate`, never `TODATE`).
17
20
  */
18
- type SqlKind = "postgres" | "mysql" | "oracle" | "sqlserver" | "dm" | "kingbase" | "doris";
21
+ type SqlKind = "postgres" | "mysql" | "oracle" | "sqlserver" | "dm" | "kingbase" | "doris" | "clickhouse";
19
22
  /**
20
23
  * One table or view as the catalog reports it. Names carry the vendor's own casing — that casing
21
24
  * is information, and everything downstream preserves it.
@@ -170,9 +173,10 @@ type Runnable = {
170
173
  */
171
174
  text: string;
172
175
  /**
173
- * The same span with everything the engine's own scanner skips whole — literals, comments and
174
- * quoted names — overwritten by spaces, character for character, so offsets still line up with
175
- * `text`. Anything reading the statement's syntax rather than its content must read this: in
176
+ * The statement as the engine's own scanner reads it when it is sent — under the lexicon the
177
+ * kind's driver finds a `?` by, from the statement's own start — with everything the scanner
178
+ * skips whole (literals, comments and quoted names) overwritten by spaces, character for
179
+ * character, so offsets still line up with `text`. A marker scan must read this: in
176
180
  * `select '?'` the `?` is prose, and a marker scan of the raw text would ask the operator for
177
181
  * a value the engine then refuses as an extra argument.
178
182
  */
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as CatalogTable, S as CatalogFunction, _ as readCall, a as createSqlAssist, b as runnableStatement, c as SqlSource, d as CallRead, f as IdentifierRead, g as TableRef, h as StatementRead, i as SqlSignatureHelp, l as createSqlSchema, m as StatementContext, n as SqlCompletionCandidate, o as SqlCatalog, p as Runnable, r as SqlHoverCard, s as SqlSchema, t as SqlAssist, u as qualifiedName, v as readIdentifier, w as SqlKind, x as CatalogColumn, y as readStatement } from "./assist-BM4bNDhw.js";
1
+ import { C as CatalogTable, S as CatalogFunction, _ as readCall, a as createSqlAssist, b as runnableStatement, c as SqlSource, d as CallRead, f as IdentifierRead, g as TableRef, h as StatementRead, i as SqlSignatureHelp, l as createSqlSchema, m as StatementContext, n as SqlCompletionCandidate, o as SqlCatalog, p as Runnable, r as SqlHoverCard, s as SqlSchema, t as SqlAssist, u as qualifiedName, v as readIdentifier, w as SqlKind, x as CatalogColumn, y as readStatement } from "./assist-Cr2idunB.js";
2
2
  //#region src/functions.d.ts
3
3
  /**
4
4
  * The function vocabulary this bench speaks: the curated floor this project wrote
@@ -78,11 +78,17 @@ interface SqlFunction {
78
78
  * manual: the words an integration engineer writes at this console, weighted toward reading — a
79
79
  * keyword or function missing here costs one suggestion, so the lists stay honest rather than
80
80
  * exhaustive. Everything is lowercase, the casing the bench itself writes (the tree's
81
- * `select * from`, the editor placeholder); completion re-cases it to follow the operator's hand.
81
+ * `select * from`, the editor placeholder); completion re-cases it to follow the operator's hand —
82
+ * but for the functions of a kind that names them by their case, spelled as the vendor spells them.
82
83
  */
83
84
  interface SqlDialect {
84
85
  keywords: string[];
85
86
  functions: SqlFunction[];
87
+ /**
88
+ * The kind names a function by its case (ClickHouse's `toDate`, never `TODATE`): its curated
89
+ * functions are written as listed, whatever the operator's hand.
90
+ */
91
+ functionsByCase?: boolean;
86
92
  }
87
93
  export declare const SQL_DIALECTS: Record<SqlKind, SqlDialect>;
88
94
  /**
@@ -133,11 +139,11 @@ export declare function joinGuesses(kind: SqlKind, target: SqlSource, others: Sq
133
139
  * Placeholder detection for what a run asks the operator for.
134
140
  *
135
141
  * This is **not** validation and must never become it: the engine-side scanner (the polyglot
136
- * sidecar's `internal/scan`) is the authority on what binds, it is lexical and per-dialect, and a second
137
- * implementation here would drift from it the first time a dialect gained a quoting form. All
138
- * this decides is whether the run has values to ask for, which form the parameter dialog
139
- * offers, and — where the statement is one the scanner refuses outright — the refusal, as a
140
- * code, so the editor can say so before the round trip does.
142
+ * sidecar's `sql/Placeholders`) is the authority on what binds, it is lexical and per-dialect,
143
+ * and a second implementation here would drift from it the first time a dialect gained a quoting
144
+ * form. All this decides is whether the run has values to ask for, which form the parameter
145
+ * dialog offers, and — where the statement is one the scanner refuses outright — the refusal, as
146
+ * a code, so the editor can say so before the round trip does.
141
147
  *
142
148
  * **Callers pass the statement's blanked form** (`Runnable.blanked`), never its raw text. A
143
149
  * disagreement with the engine's scanner is not cosmetic in either direction — it answers both
@@ -146,8 +152,10 @@ export declare function joinGuesses(kind: SqlKind, target: SqlSource, others: Sq
146
152
  * has no placeholder in it. Blanking is the reader's job because the reader already owns what is
147
153
  * prose and what is SQL for this page, and it takes that decision under the data source's own
148
154
  * lexical profile (`noise.ts`), mode for mode with the scanner — MySQL's `#` comments and
149
- * backslash escapes, Postgres's `$tag$` bodies, Oracle's `q'…'`, every kind's quoted names — so
150
- * what reaches this file is exactly the text the engine would mint markers from.
155
+ * backslash escapes, Postgres's `$tag$` bodies, Oracle's `q'…'`, every kind's quoted names,
156
+ * SQL Server's block comments as its driver ends them, and ClickHouse's conditional operator as
157
+ * its driver crosses it — so what reaches this file is exactly the text the engine would mint
158
+ * markers from.
151
159
  */
152
160
  /**
153
161
  * The binding form a statement's markers suggest.
@@ -159,8 +167,8 @@ type MarkerForm = "none" | "positional" | "named";
159
167
  *
160
168
  * - `"mixed-forms"` — the statement writes both `?` and `:name`; the scanner binds neither.
161
169
  * - `"question-escape-unsupported"` — `??` on a kind whose native form is itself `?` (MySQL,
162
- * 达梦): the escape has no rendering that survives, and neither dialect has a `?` operator to
163
- * escape in the first place.
170
+ * 达梦, ClickHouse): the escape has no rendering that survives, and none is needed — MySQL and
171
+ * 达梦 have no `?` operator, and ClickHouse's one, the conditional `a ? b : c`, is read as one.
164
172
  * - `"native-marker-collision"` — a marker hand-written in the kind's own bind form (`$1`,
165
173
  * `:1`, `@p1`) beside a `?`/`:name` the scanner would mint: both would bind the same slot.
166
174
  */
@@ -191,6 +199,11 @@ interface StatementMarkers {
191
199
  export declare function statementMarkers(statement: string, kind: SqlKind): StatementMarkers;
192
200
  //#endregion
193
201
  //#region src/noise.d.ts
202
+ /**
203
+ * Which lexicon a text is read under: the database's own — where a statement ends, what is prose
204
+ * — or the one the kind's driver finds a `?` by, which the sidecar mints its markers under.
205
+ */
206
+ type NoiseLexicon = "database" | "driver";
194
207
  /**
195
208
  * A run the reader must not look inside. `sealed` says the closing delimiter was found: a caret
196
209
  * right after a sealed run is back in the statement, while a caret at the end of an unterminated
@@ -223,9 +236,10 @@ interface BlankedSql {
223
236
  /**
224
237
  * The statement with its noise blanked out, char for char, so every offset survives — in the two
225
238
  * renderings the reader needs (see [BlankedSql]). One pass, mirroring the sidecar scanner's own
226
- * loop over its mutually exclusive lexical modes.
239
+ * loop over its mutually exclusive lexical modes, under the database's own lexicon unless the
240
+ * driver's is asked for.
227
241
  */
228
- export declare function blankNoise(text: string, kind: SqlKind): BlankedSql;
242
+ export declare function blankNoise(text: string, kind: SqlKind, lexicon?: NoiseLexicon): BlankedSql;
229
243
  /**
230
244
  * Whether the caret sits inside a string or a comment, where the bench has nothing to say. A
231
245
  * quoted name is not one: it is a name the operator is writing, and the reader is about names.
@@ -373,4 +387,4 @@ export declare function spellQualifiedName(kind: SqlKind, table: {
373
387
  name: string;
374
388
  }): string;
375
389
  //#endregion
376
- export { type BenchFunction, type BindRefusal, type BlankedSql, type Bound, type BoundParams, type CallRead, type CatalogColumn, type CatalogFunction, type CatalogTable, type IdentifierRead, type JoinGuess, type JoinReason, type MarkerForm, type MarkerRefusal, type NoiseSpan, type ParamType, type ParamValue, type Runnable, type SqlAssist, type SqlCatalog, type SqlCompletionCandidate, type SqlDialect, type SqlFunction, type SqlFunctions, type SqlHoverCard, type SqlKind, type SqlScalar, type SqlSchema, type SqlSignatureHelp, type SqlSource, type StatementContext, type StatementMarkers, type StatementOrigin, type StatementRead, type TableRef, createSqlAssist, createSqlSchema, qualifiedName, readCall, readIdentifier, readStatement, runnableStatement };
390
+ export { type BenchFunction, type BindRefusal, type BlankedSql, type Bound, type BoundParams, type CallRead, type CatalogColumn, type CatalogFunction, type CatalogTable, type IdentifierRead, type JoinGuess, type JoinReason, type MarkerForm, type MarkerRefusal, type NoiseLexicon, type NoiseSpan, type ParamType, type ParamValue, type Runnable, type SqlAssist, type SqlCatalog, type SqlCompletionCandidate, type SqlDialect, type SqlFunction, type SqlFunctions, type SqlHoverCard, type SqlKind, type SqlScalar, type SqlSchema, type SqlSignatureHelp, type SqlSource, type StatementContext, type StatementMarkers, type StatementOrigin, type StatementRead, type TableRef, createSqlAssist, createSqlSchema, qualifiedName, readCall, readIdentifier, readStatement, runnableStatement };
package/dist/index.js CHANGED
@@ -181,6 +181,63 @@ const MYSQL = dialect([
181
181
  fn("group_concat", "group_concat(expr)", "Aggregate: joins each row's value into one string; takes `separator '…'`."),
182
182
  fn("json_extract", "json_extract(json, path)", "Reads a JSON path, as in `'$.id'`.")
183
183
  ]);
184
+ const CLICKHOUSE$1 = {
185
+ ...dialect([
186
+ "final",
187
+ "prewhere",
188
+ "sample",
189
+ "array",
190
+ "global",
191
+ "any",
192
+ "asof",
193
+ "semi",
194
+ "anti",
195
+ "limit",
196
+ "offset",
197
+ "settings",
198
+ "format",
199
+ "totals",
200
+ "rollup",
201
+ "cube",
202
+ "ilike",
203
+ "interval",
204
+ "true",
205
+ "false"
206
+ ], [
207
+ fn("now", "now(timezone)", "Current date and time, in the server's time zone unless one is named."),
208
+ fn("today", "today()", "Current date."),
209
+ fn("toDate", "toDate(value)", "Converts to a Date."),
210
+ fn("toDateTime", "toDateTime(value, timezone)", "Converts to a DateTime, in a time zone where one is named."),
211
+ fn("toString", "toString(value)", "Converts to a String."),
212
+ fn("toInt64", "toInt64(value)", "Converts to a 64-bit integer."),
213
+ fn("toFloat64", "toFloat64(value)", "Converts to a 64-bit float."),
214
+ fn("toStartOfDay", "toStartOfDay(time)", "Rounds a time down to the start of its day."),
215
+ fn("toStartOfHour", "toStartOfHour(time)", "Rounds a time down to the start of its hour."),
216
+ fn("toStartOfMonth", "toStartOfMonth(date)", "Rounds a date down to the first day of its month."),
217
+ fn("formatDateTime", "formatDateTime(time, format)", "Formats a time through a format string, as in `'%Y-%m-%d'`."),
218
+ fn("dateDiff", "dateDiff(unit, start, end)", "Unit boundaries crossed between two times, as in `dateDiff('day', a, b)`."),
219
+ fn("if", "if(condition, then, else)", "Ternary choice."),
220
+ fn("multiIf", "multiIf(condition, then, …, else)", "The value of the first condition that holds, or the last argument."),
221
+ fn("ifNull", "ifNull(value, fallback)", "The second argument when the first is NULL."),
222
+ fn("concat", "concat(value, …)", "Joins the arguments as strings."),
223
+ fn("length", "length(value)", "Bytes in a string, elements in an array; `lengthUTF8` counts characters."),
224
+ fn("substring", "substring(text, offset, length)", "`length` bytes from byte `offset`, counting from 1; `substringUTF8` counts characters."),
225
+ fn("position", "position(text, search)", "Byte position of a substring, counting from 1, or 0 when it is absent."),
226
+ fn("replaceAll", "replaceAll(text, search, replacement)", "Replaces every occurrence of a substring."),
227
+ fn("splitByChar", "splitByChar(separator, text)", "Splits a string on a one-character separator into an array."),
228
+ fn("JSONExtractString", "JSONExtractString(json, key, …)", "A string read out of JSON by key or index, empty where there is none."),
229
+ fn("has", "has(array, value)", "Whether the array holds the value."),
230
+ fn("arrayJoin", "arrayJoin(array)", "One row per element of the array."),
231
+ fn("countIf", "countIf(condition)", "Aggregate: rows for which the condition holds."),
232
+ fn("sumIf", "sumIf(value, condition)", "Aggregate: the sum over the rows for which the condition holds."),
233
+ fn("uniq", "uniq(expr)", "Aggregate: the distinct values, counted approximately."),
234
+ fn("uniqExact", "uniqExact(expr)", "Aggregate: the distinct values, counted exactly."),
235
+ fn("groupArray", "groupArray(expr)", "Aggregate: collects each row's value into an array."),
236
+ fn("argMax", "argMax(value, by)", "Aggregate: `value` from the row where `by` is largest."),
237
+ fn("any", "any(expr)", "Aggregate: one value of the group, whichever is met first.")
238
+ ]),
239
+ functionsByCase: true
240
+ };
184
241
  const SQL_DIALECTS = {
185
242
  postgres: POSTGRES$1,
186
243
  kingbase: POSTGRES$1,
@@ -222,7 +279,8 @@ const SQL_DIALECTS = {
222
279
  fn("rtrim", "rtrim(text)", "Strips trailing whitespace."),
223
280
  fn("string_agg", "string_agg(expr, delimiter)", "Aggregate: joins each row's value with a delimiter.")
224
281
  ]),
225
- dm: ORACLE$1
282
+ dm: ORACLE$1,
283
+ clickhouse: CLICKHOUSE$1
226
284
  };
227
285
  function signatureParameters(signature) {
228
286
  const open = signature.indexOf("(");
@@ -323,6 +381,13 @@ const TSQL_RESERVED = reservedWords(`
323
381
  tran transaction trigger truncate try_convert tsequal union unique unpivot update updatetext
324
382
  use user values varying view waitfor when where while with within writetext
325
383
  `);
384
+ const CLICKHOUSE_RESERVED = reservedWords(`
385
+ all and anti any array as asc asof between by case cast collate cross cube desc distinct else
386
+ end except exists false fill final first format from full global group having ilike in inner
387
+ insert interpolate intersect interval into is join last left like limit not null nulls offset
388
+ on or order outer paste prewhere qualify right rollup sample select semi settings step table
389
+ then totals true union using values when where window with
390
+ `);
326
391
  const ORACLE = {
327
392
  open: "\"",
328
393
  close: "\"",
@@ -357,10 +422,18 @@ const SPELLINGS = {
357
422
  regular: /^[a-z_@#][\w@#$]*$/i,
358
423
  reserved: TSQL_RESERVED
359
424
  },
360
- dm: ORACLE
425
+ dm: ORACLE,
426
+ clickhouse: {
427
+ open: "`",
428
+ close: "`",
429
+ backslash: true,
430
+ regular: /^[a-z_]\w*$/i,
431
+ reserved: CLICKHOUSE_RESERVED
432
+ }
361
433
  };
362
434
  function delimited(spelling, name) {
363
- return `${spelling.open}${name.replaceAll(spelling.close, () => spelling.close + spelling.close)}${spelling.close}`;
435
+ const escaped = spelling.backslash === true ? name.replaceAll("\\", () => "\\\\") : name;
436
+ return `${spelling.open}${escaped.replaceAll(spelling.close, () => spelling.close + spelling.close)}${spelling.close}`;
364
437
  }
365
438
  function spellIdentifier(kind, name) {
366
439
  const spelling = SPELLINGS[kind];
@@ -421,6 +494,67 @@ function joinGuesses(kind, target, others) {
421
494
  return [...found.values()];
422
495
  }
423
496
  //#endregion
497
+ //#region src/conditional.ts
498
+ function conditionalEnd(text, at) {
499
+ let index = at + 2;
500
+ while (index >= 0 && index < text.length) {
501
+ const ch = text[index];
502
+ if (ch === "?" || ch === ":") return ch === ":" && index + 1 < text.length && text[index + 1] !== ":" && text[index - 1] !== ":" ? index + 1 : -1;
503
+ index = skipped(text, index);
504
+ }
505
+ return -1;
506
+ }
507
+ function skipped(text, at) {
508
+ const ch = text[at];
509
+ if (ch === "'" || ch === "\"" || ch === "`") return quoted(text, at);
510
+ if (ch === "(" || ch === "[" || ch === "{") return group(text, at);
511
+ if (ch === "-" && text[at + 1] === "-") return lineEnd(text, at + 2);
512
+ if (ch === "/" && text[at + 1] === "*") return blockEnd(text, at + 2);
513
+ return at + 1;
514
+ }
515
+ function quoted(text, open) {
516
+ const quote = text[open];
517
+ for (let index = open + 1; index < text.length; index += 1) {
518
+ const ch = text[index];
519
+ if (ch === "\\") index += 1;
520
+ else if (ch === quote) {
521
+ if (text[index + 1] !== quote) return index + 1;
522
+ index += 1;
523
+ }
524
+ }
525
+ return -1;
526
+ }
527
+ const CLOSERS = {
528
+ "(": ")",
529
+ "[": "]",
530
+ "{": "}"
531
+ };
532
+ function group(text, open) {
533
+ const close = CLOSERS[text[open]];
534
+ let index = open + 1;
535
+ while (index >= 0 && index < text.length) {
536
+ if (text[index] === close) return index + 1;
537
+ index = skipped(text, index);
538
+ }
539
+ return -1;
540
+ }
541
+ function lineEnd(text, from) {
542
+ const end = text.indexOf("\n", from);
543
+ return end > from ? end + 1 : text.length;
544
+ }
545
+ function blockEnd(text, from) {
546
+ let depth = 1;
547
+ for (let index = from; index + 1 < text.length; index += 1) if (text[index] === "/" && text[index + 1] === "*") {
548
+ depth += 1;
549
+ index += 1;
550
+ } else if (text[index] === "*" && text[index + 1] === "/") {
551
+ depth -= 1;
552
+ index += 1;
553
+ if (depth === 0) return index + 1;
554
+ }
555
+ return -1;
556
+ }
557
+ //#endregion
424
558
  //#region src/noise.ts
425
559
  const BASE = {
426
560
  backslashEscapes: false,
@@ -432,7 +566,22 @@ const BASE = {
432
566
  dollarQuoting: false,
433
567
  escapeStrings: false,
434
568
  nestedBlockComments: false,
435
- alternativeQuoting: false
569
+ alternativeQuoting: false,
570
+ backslashInBackticks: false,
571
+ conditionalOperator: false
572
+ };
573
+ const SQLSERVER = {
574
+ ...BASE,
575
+ bracketIdentifiers: true,
576
+ nestedBlockComments: true
577
+ };
578
+ const CLICKHOUSE = {
579
+ ...BASE,
580
+ backslashEscapes: true,
581
+ hashLineComments: true,
582
+ backtickIdentifiers: true,
583
+ nestedBlockComments: true,
584
+ backslashInBackticks: true
436
585
  };
437
586
  const NOISE_PROFILES = {
438
587
  postgres: {
@@ -461,16 +610,24 @@ const NOISE_PROFILES = {
461
610
  ...BASE,
462
611
  alternativeQuoting: true
463
612
  },
464
- sqlserver: {
465
- ...BASE,
466
- bracketIdentifiers: true
467
- },
613
+ sqlserver: SQLSERVER,
468
614
  dm: BASE,
469
615
  kingbase: {
470
616
  ...BASE,
471
617
  escapeStrings: true,
472
618
  nestedBlockComments: true,
473
619
  dollarQuoting: true
620
+ },
621
+ clickhouse: CLICKHOUSE
622
+ };
623
+ const DRIVER_PROFILES = {
624
+ sqlserver: {
625
+ ...SQLSERVER,
626
+ nestedBlockComments: false
627
+ },
628
+ clickhouse: {
629
+ ...CLICKHOUSE,
630
+ conditionalOperator: true
474
631
  }
475
632
  };
476
633
  const WORD_START = /[a-z_\u{0080}-\u{FFFF}]/iu;
@@ -575,10 +732,14 @@ function opensLineComment(text, from, profile) {
575
732
  const after = text[from + 2];
576
733
  return !profile.lineCommentNeedsSpace || after === void 0 || after <= " ";
577
734
  }
578
- let lastBlanked = null;
579
- function blankNoise(text, kind) {
580
- if (lastBlanked !== null && lastBlanked.text === text && lastBlanked.kind === kind) return lastBlanked.result;
581
- const profile = NOISE_PROFILES[kind];
735
+ const lastBlanked = {
736
+ database: null,
737
+ driver: null
738
+ };
739
+ function blankNoise(text, kind, lexicon = "database") {
740
+ const last = lastBlanked[lexicon];
741
+ if (last !== null && last.text === text && last.kind === kind) return last.result;
742
+ const profile = lexicon === "driver" ? DRIVER_PROFILES[kind] ?? NOISE_PROFILES[kind] : NOISE_PROFILES[kind];
582
743
  const withNames = text.split("");
583
744
  const blanked = text.split("");
584
745
  const spans = [];
@@ -600,11 +761,18 @@ function blankNoise(text, kind) {
600
761
  const ch = text[at];
601
762
  if (ch === "'") skip(closeDelimited(text, at + 1, "'", profile.backslashEscapes), false);
602
763
  else if (ch === "\"") skip(closeDelimited(text, at + 1, "\"", profile.backslashEscapes), !profile.doubleQuotedStrings);
603
- else if (ch === "`" && profile.backtickIdentifiers) skip(closeDelimited(text, at + 1, "`", false), true);
764
+ else if (ch === "`" && profile.backtickIdentifiers) skip(closeDelimited(text, at + 1, "`", profile.backslashInBackticks), true);
604
765
  else if (ch === "[" && profile.bracketIdentifiers) skip(closeBracket(text, at + 1), true);
605
766
  else if (opensLineComment(text, at, profile)) skip(closeLine(text, at), false);
606
767
  else if (ch === "/" && text[at + 1] === "*") skip(closeBlock(text, at + 2, profile.nestedBlockComments), false);
607
- else if (ch === "$" && profile.dollarQuoting) {
768
+ else if (ch === "?" && profile.conditionalOperator) {
769
+ const end = conditionalEnd(text, at);
770
+ if (end < 0) at += text[at + 1] === "?" ? 2 : 1;
771
+ else skip({
772
+ end,
773
+ sealed: true
774
+ }, false);
775
+ } else if (ch === "$" && profile.dollarQuoting) {
608
776
  const quoted = closeDollarQuoted(text, at);
609
777
  if (quoted === null) at += 1;
610
778
  else skip(quoted, false);
@@ -622,7 +790,7 @@ function blankNoise(text, kind) {
622
790
  blanked: blanked.join(""),
623
791
  spans
624
792
  };
625
- lastBlanked = {
793
+ lastBlanked[lexicon] = {
626
794
  text,
627
795
  kind,
628
796
  result
@@ -918,10 +1086,11 @@ function runnableStatement(text, from, to, kind) {
918
1086
  if (spans.length === 0) return { kind: "none" };
919
1087
  if (selected && spans.length > 1) return { kind: "several" };
920
1088
  const span = spans[0];
1089
+ const statement = text.slice(span.from, span.to);
921
1090
  return {
922
1091
  kind: "one",
923
- text: text.slice(span.from, span.to),
924
- blanked: blanked.slice(span.from, span.to),
1092
+ text: statement,
1093
+ blanked: blankNoise(statement, kind, "driver").blanked,
925
1094
  from: span.from,
926
1095
  to: span.to,
927
1096
  alone: statementSpans(text, blanked, 0, text.length).length <= 1
@@ -1023,7 +1192,7 @@ function expansionCandidate(kind, sources, replacesStar) {
1023
1192
  };
1024
1193
  }
1025
1194
  function createSqlCompletion(kind, schema, functions) {
1026
- const { keywords } = SQL_DIALECTS[kind];
1195
+ const { keywords, functionsByCase = false } = SQL_DIALECTS[kind];
1027
1196
  const spelled = /* @__PURE__ */ new Map();
1028
1197
  async function wordCandidates(typed) {
1029
1198
  const upper = typed !== "" && typed === typed.toUpperCase() && typed !== typed.toLowerCase();
@@ -1039,7 +1208,7 @@ function createSqlCompletion(kind, schema, functions) {
1039
1208
  sortGroup: RANK_KEYWORD
1040
1209
  };
1041
1210
  }), ...known.map((entry) => {
1042
- const name = entry.origin === "curated" ? spell(entry.name) : entry.name;
1211
+ const name = entry.origin === "curated" && !functionsByCase ? spell(entry.name) : entry.name;
1043
1212
  const written = entry.origin === "curated" ? name : spellCallable(kind, name);
1044
1213
  const call = entry.qualifier === "" ? written : `${spellIdentifier(kind, entry.qualifier)}.${written}`;
1045
1214
  return {
@@ -1161,7 +1330,8 @@ const DIALECTS = {
1161
1330
  oracle: "plsql",
1162
1331
  dm: "plsql",
1163
1332
  sqlserver: "transactsql",
1164
- kingbase: "postgresql"
1333
+ kingbase: "postgresql",
1334
+ clickhouse: "clickhouse"
1165
1335
  };
1166
1336
  const PLACEHOLDERS = String.raw`\?|:[A-Za-z_]\w*`;
1167
1337
  function sentinelFor(text) {
@@ -1426,8 +1596,12 @@ function createSqlAssist(kind, catalog) {
1426
1596
  }
1427
1597
  //#endregion
1428
1598
  //#region src/markers.ts
1429
- const SCAN = /[A-Z_\u0080-\uFFFF][\w$\u0080-\uFFFF]*|(?<qq>\?\?)|::|(?<q>\?)|:(?<name>[A-Z_]\w*)|(?<colonDigits>:\d+)|(?<dollarDigits>\$\d+)|(?<atDigits>@P\d+)/gi;
1430
- const QUESTION_IS_NATIVE = /* @__PURE__ */ new Set(["mysql", "dm"]);
1599
+ const SCAN = /[A-Z_\u0080-\uFFFF][\w$\u0080-\uFFFF]*|(?<qq>\?\?)|::|(?<q>\?)|(?<!\.):(?<name>[A-Z_]\w*)|(?<colonDigits>:\d+)|(?<dollarDigits>\$\d+)|(?<atDigits>@P\d+)/gi;
1600
+ const QUESTION_IS_NATIVE = /* @__PURE__ */ new Set([
1601
+ "mysql",
1602
+ "dm",
1603
+ "clickhouse"
1604
+ ]);
1431
1605
  const NATIVE_GROUP = {
1432
1606
  postgres: "dollarDigits",
1433
1607
  mysql: null,
@@ -1435,7 +1609,8 @@ const NATIVE_GROUP = {
1435
1609
  oracle: "colonDigits",
1436
1610
  sqlserver: "atDigits",
1437
1611
  dm: null,
1438
- kingbase: "dollarDigits"
1612
+ kingbase: "dollarDigits",
1613
+ clickhouse: null
1439
1614
  };
1440
1615
  function statementMarkers(statement, kind) {
1441
1616
  const names = [];
package/dist/monaco.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as SqlAssist } from "./assist-BM4bNDhw.js";
1
+ import { t as SqlAssist } from "./assist-Cr2idunB.js";
2
2
  import * as monacoEditor from "monaco-editor";
3
3
  import { IDisposable } from "monaco-editor";
4
4
  //#region src/monaco.d.ts
package/dist/react.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as SqlAssist } from "./assist-BM4bNDhw.js";
1
+ import { t as SqlAssist } from "./assist-Cr2idunB.js";
2
2
  import { ConsoleCaret, ConsoleEditorHandle, ConsoleEditorProps } from "@coldsmirk/inkstone-react/monaco";
3
3
  import { ReactNode } from "react";
4
4
  //#region src/react.d.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coldsmirk/inkstone-sql",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "description": "Schema-aware SQL editing intelligence for Monaco: lexical statement reading under per-dialect noise profiles, catalog-driven completion/hover/signature help, unified-placeholder awareness, formatting, and a drop-in React SQL editor.",
5
5
  "keywords": [
6
6
  "sql",
@@ -48,12 +48,12 @@
48
48
  "devDependencies": {
49
49
  "monaco-editor": "^0.57.0",
50
50
  "react": "^19.3.0",
51
- "@coldsmirk/inkstone-react": "^0.32.0"
51
+ "@coldsmirk/inkstone-react": "^0.34.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "monaco-editor": "^0.57.0",
55
55
  "react": ">=19",
56
- "@coldsmirk/inkstone-react": "^0.32.0"
56
+ "@coldsmirk/inkstone-react": "^0.34.0"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "@coldsmirk/inkstone-react": {