@coldsmirk/inkstone-sql 0.31.0 → 0.33.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
@@ -8,7 +8,7 @@ Part of [inkstone](https://github.com/coldsmirk/inkstone). Extracted from dragom
8
8
 
9
9
  - **`@coldsmirk/inkstone-sql`** — the framework-free core. Zero editor dependencies; runs anywhere.
10
10
  - **`@coldsmirk/inkstone-sql/monaco`** — `registerSqlLanguage(monaco, modelPath, hooks)`: the Monaco providers (completion, hover, signature help, formatting) over an assist read per-request through a hook. Peer: `monaco-editor`.
11
- - **`@coldsmirk/inkstone-sql/react`** — `<SqlEditor>`: a controlled SQL editor built on `@coldsmirk/inkstone-react`'s `<MonacoEditor>`, with a run key (Ctrl/Cmd-Enter handing over the live caret), a runnable-statement gutter mark, caret tracking, and an insert/format handle for schema trees and toolbars. Peers: `monaco-editor`, `react`.
11
+ - **`@coldsmirk/inkstone-sql/react`** — `<SqlEditor>`: a controlled SQL editor built on `@coldsmirk/inkstone-react`'s `<MonacoConsoleEditor>`, with a run key (Ctrl/Cmd-Enter handing over the live caret), a runnable-statement gutter mark, caret tracking, and an insert/focus/format handle for schema trees and toolbars. Peers: `monaco-editor`, `react`.
12
12
 
13
13
  ## The shape
14
14
 
@@ -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. 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,9 @@ 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, and
156
+ * SQL Server's block comments as its driver ends them — so what reaches this file is exactly the
157
+ * text the engine would mint markers from.
151
158
  */
152
159
  /**
153
160
  * The binding form a statement's markers suggest.
@@ -159,8 +166,8 @@ type MarkerForm = "none" | "positional" | "named";
159
166
  *
160
167
  * - `"mixed-forms"` — the statement writes both `?` and `:name`; the scanner binds neither.
161
168
  * - `"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.
169
+ * 达梦, ClickHouse): the escape has no rendering that survives, and none of them has a `?`
170
+ * operator to escape in the first place.
164
171
  * - `"native-marker-collision"` — a marker hand-written in the kind's own bind form (`$1`,
165
172
  * `:1`, `@p1`) beside a `?`/`:name` the scanner would mint: both would bind the same slot.
166
173
  */
@@ -191,6 +198,11 @@ interface StatementMarkers {
191
198
  export declare function statementMarkers(statement: string, kind: SqlKind): StatementMarkers;
192
199
  //#endregion
193
200
  //#region src/noise.d.ts
201
+ /**
202
+ * Which lexicon a text is read under: the database's own — where a statement ends, what is prose
203
+ * — or the one the kind's driver finds a `?` by, which the sidecar mints its markers under.
204
+ */
205
+ type NoiseLexicon = "database" | "driver";
194
206
  /**
195
207
  * A run the reader must not look inside. `sealed` says the closing delimiter was found: a caret
196
208
  * right after a sealed run is back in the statement, while a caret at the end of an unterminated
@@ -223,9 +235,10 @@ interface BlankedSql {
223
235
  /**
224
236
  * The statement with its noise blanked out, char for char, so every offset survives — in the two
225
237
  * renderings the reader needs (see [BlankedSql]). One pass, mirroring the sidecar scanner's own
226
- * loop over its mutually exclusive lexical modes.
238
+ * loop over its mutually exclusive lexical modes, under the database's own lexicon unless the
239
+ * driver's is asked for.
227
240
  */
228
- export declare function blankNoise(text: string, kind: SqlKind): BlankedSql;
241
+ export declare function blankNoise(text: string, kind: SqlKind, lexicon?: NoiseLexicon): BlankedSql;
229
242
  /**
230
243
  * Whether the caret sits inside a string or a comment, where the bench has nothing to say. A
231
244
  * quoted name is not one: it is a name the operator is writing, and the reader is about names.
@@ -373,4 +386,4 @@ export declare function spellQualifiedName(kind: SqlKind, table: {
373
386
  name: string;
374
387
  }): string;
375
388
  //#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 };
389
+ 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 = {
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
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];
@@ -434,6 +507,11 @@ const BASE = {
434
507
  nestedBlockComments: false,
435
508
  alternativeQuoting: false
436
509
  };
510
+ const SQLSERVER = {
511
+ ...BASE,
512
+ bracketIdentifiers: true,
513
+ nestedBlockComments: true
514
+ };
437
515
  const NOISE_PROFILES = {
438
516
  postgres: {
439
517
  ...BASE,
@@ -461,18 +539,25 @@ const NOISE_PROFILES = {
461
539
  ...BASE,
462
540
  alternativeQuoting: true
463
541
  },
464
- sqlserver: {
465
- ...BASE,
466
- bracketIdentifiers: true
467
- },
542
+ sqlserver: SQLSERVER,
468
543
  dm: BASE,
469
544
  kingbase: {
470
545
  ...BASE,
471
546
  escapeStrings: true,
472
547
  nestedBlockComments: true,
473
548
  dollarQuoting: true
549
+ },
550
+ clickhouse: {
551
+ ...BASE,
552
+ backslashEscapes: true,
553
+ hashLineComments: true,
554
+ backtickIdentifiers: true
474
555
  }
475
556
  };
557
+ const DRIVER_PROFILES = { sqlserver: {
558
+ ...SQLSERVER,
559
+ nestedBlockComments: false
560
+ } };
476
561
  const WORD_START = /[a-z_\u{0080}-\u{FFFF}]/iu;
477
562
  const WORD_BYTE = /[\w$\u{0080}-\u{FFFF}]/u;
478
563
  const NAME_BYTE = /\w/;
@@ -575,10 +660,14 @@ function opensLineComment(text, from, profile) {
575
660
  const after = text[from + 2];
576
661
  return !profile.lineCommentNeedsSpace || after === void 0 || after <= " ";
577
662
  }
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];
663
+ const lastBlanked = {
664
+ database: null,
665
+ driver: null
666
+ };
667
+ function blankNoise(text, kind, lexicon = "database") {
668
+ const last = lastBlanked[lexicon];
669
+ if (last !== null && last.text === text && last.kind === kind) return last.result;
670
+ const profile = lexicon === "driver" ? DRIVER_PROFILES[kind] ?? NOISE_PROFILES[kind] : NOISE_PROFILES[kind];
582
671
  const withNames = text.split("");
583
672
  const blanked = text.split("");
584
673
  const spans = [];
@@ -622,7 +711,7 @@ function blankNoise(text, kind) {
622
711
  blanked: blanked.join(""),
623
712
  spans
624
713
  };
625
- lastBlanked = {
714
+ lastBlanked[lexicon] = {
626
715
  text,
627
716
  kind,
628
717
  result
@@ -918,10 +1007,11 @@ function runnableStatement(text, from, to, kind) {
918
1007
  if (spans.length === 0) return { kind: "none" };
919
1008
  if (selected && spans.length > 1) return { kind: "several" };
920
1009
  const span = spans[0];
1010
+ const statement = text.slice(span.from, span.to);
921
1011
  return {
922
1012
  kind: "one",
923
- text: text.slice(span.from, span.to),
924
- blanked: blanked.slice(span.from, span.to),
1013
+ text: statement,
1014
+ blanked: blankNoise(statement, kind, "driver").blanked,
925
1015
  from: span.from,
926
1016
  to: span.to,
927
1017
  alone: statementSpans(text, blanked, 0, text.length).length <= 1
@@ -1023,7 +1113,7 @@ function expansionCandidate(kind, sources, replacesStar) {
1023
1113
  };
1024
1114
  }
1025
1115
  function createSqlCompletion(kind, schema, functions) {
1026
- const { keywords } = SQL_DIALECTS[kind];
1116
+ const { keywords, functionsByCase = false } = SQL_DIALECTS[kind];
1027
1117
  const spelled = /* @__PURE__ */ new Map();
1028
1118
  async function wordCandidates(typed) {
1029
1119
  const upper = typed !== "" && typed === typed.toUpperCase() && typed !== typed.toLowerCase();
@@ -1039,7 +1129,7 @@ function createSqlCompletion(kind, schema, functions) {
1039
1129
  sortGroup: RANK_KEYWORD
1040
1130
  };
1041
1131
  }), ...known.map((entry) => {
1042
- const name = entry.origin === "curated" ? spell(entry.name) : entry.name;
1132
+ const name = entry.origin === "curated" && !functionsByCase ? spell(entry.name) : entry.name;
1043
1133
  const written = entry.origin === "curated" ? name : spellCallable(kind, name);
1044
1134
  const call = entry.qualifier === "" ? written : `${spellIdentifier(kind, entry.qualifier)}.${written}`;
1045
1135
  return {
@@ -1161,7 +1251,8 @@ const DIALECTS = {
1161
1251
  oracle: "plsql",
1162
1252
  dm: "plsql",
1163
1253
  sqlserver: "transactsql",
1164
- kingbase: "postgresql"
1254
+ kingbase: "postgresql",
1255
+ clickhouse: "clickhouse"
1165
1256
  };
1166
1257
  const PLACEHOLDERS = String.raw`\?|:[A-Za-z_]\w*`;
1167
1258
  function sentinelFor(text) {
@@ -1427,7 +1518,11 @@ function createSqlAssist(kind, catalog) {
1427
1518
  //#endregion
1428
1519
  //#region src/markers.ts
1429
1520
  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"]);
1521
+ const QUESTION_IS_NATIVE = /* @__PURE__ */ new Set([
1522
+ "mysql",
1523
+ "dm",
1524
+ "clickhouse"
1525
+ ]);
1431
1526
  const NATIVE_GROUP = {
1432
1527
  postgres: "dollarDigits",
1433
1528
  mysql: null,
@@ -1435,7 +1530,8 @@ const NATIVE_GROUP = {
1435
1530
  oracle: "colonDigits",
1436
1531
  sqlserver: "atDigits",
1437
1532
  dm: null,
1438
- kingbase: "dollarDigits"
1533
+ kingbase: "dollarDigits",
1534
+ clickhouse: null
1439
1535
  };
1440
1536
  function statementMarkers(statement, kind) {
1441
1537
  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,92 +1,33 @@
1
- import { t as SqlAssist } from "./assist-BM4bNDhw.js";
2
- import { MonacoModule } from "./monaco.js";
1
+ import { t as SqlAssist } from "./assist-Cr2idunB.js";
2
+ import { ConsoleCaret, ConsoleEditorHandle, ConsoleEditorProps } from "@coldsmirk/inkstone-react/monaco";
3
3
  import { ReactNode } from "react";
4
- import { editor } from "monaco-editor";
5
4
  //#region src/react.d.ts
6
5
  /**
7
6
  * What a host needs to reach into the editor for: inserting at the caret (a schema tree is an
8
- * input device, not a browser), and reformatting from a toolbar — which goes through Monaco's own
9
- * formatting action, so the button and `Shift+Alt+F` are one path and one undo.
7
+ * input device, not a browser), the focus, and reformatting from a toolbar — which goes through
8
+ * Monaco's own formatting action, so the button and `Shift+Alt+F` are one path and one undo.
10
9
  */
11
- export interface SqlEditorHandle {
12
- insert: (text: string) => void;
10
+ export interface SqlEditorHandle extends ConsoleEditorHandle {
13
11
  format: () => void;
14
12
  }
15
13
  /**
16
14
  * Where the operator is in the document, in offsets — everything a host needs to decide which
17
- * statement a run should send, and nothing about SQL. `from` and `to` are the selection's own
18
- * bounds and are equal where nothing is selected: a caret is a selection of nothing.
19
- *
20
- * It rides with the text rather than beside it because the two must belong to the same instant.
21
- * The controlled `value` is one React render behind the keystroke that triggered a run, and a
22
- * console must run the statement the operator is looking at.
15
+ * statement a run should send, and nothing about SQL.
23
16
  */
24
- export interface SqlCaret {
25
- text: string;
26
- from: number;
27
- to: number;
28
- }
29
- export interface SqlEditorProps {
30
- value: string;
31
- onChange: (value: string) => void;
32
- /**
33
- * Render with the dark theme. Like every inkstone editor, the component never reads the OS
34
- * `prefers-color-scheme`; wire this to the app's color-scheme state.
35
- *
36
- * @default false
37
- */
38
- dark?: boolean;
39
- /**
40
- * Editor height in px, or `"fill"` to consume the parent's resolved height (the parent must be
41
- * a `min-height: 0` flex item). Monaco's loading placeholder is `height: 100%`, so a height
42
- * that resolves to nothing collapses the field to one line while the editor loads.
43
- */
44
- height?: number | "fill";
45
- /**
46
- * Editor font size in px. Omit to keep the inkstone default.
47
- */
48
- fontSize?: number;
49
- /**
50
- * UI locale for Monaco's built-in chrome — page-global and locked by the first editor to
51
- * mount, exactly as on `<MonacoEditor>`.
52
- */
53
- locale?: "en" | "zh-cn";
54
- placeholder?: string;
55
- /**
56
- * The editor's accessible name, set on Monaco's own input surface — several SQL editors on one
57
- * page are indistinguishable to assistive tech without one.
58
- */
59
- ariaLabel: string;
17
+ export type SqlCaret = ConsoleCaret;
18
+ export interface SqlEditorProps extends ConsoleEditorProps<SqlEditorHandle> {
60
19
  /**
61
20
  * The Monaco language id the model registers under.
62
21
  *
63
22
  * @default "sql"
64
23
  */
65
24
  languageId?: string;
66
- /**
67
- * Shown while the Monaco host loads / when it fails to load — passed through to
68
- * `<MonacoEditor>`, whose English defaults apply when omitted.
69
- */
70
- loading?: ReactNode;
71
- failure?: ReactNode;
72
- /**
73
- * Ctrl/Cmd-Enter — the run key, bound inside the editor because that is where the operator's
74
- * hands are. It is handed the editor's live caret, from which the host works out what to send.
75
- * Read through a ref at every press, so it always runs against the connection the *current*
76
- * render is bound to.
77
- */
78
- onRun?: (caret: SqlCaret) => void;
79
25
  /**
80
26
  * The label the run action shows in Monaco's own chrome (command palette, context menu).
81
27
  *
82
28
  * @default "Run statement"
83
29
  */
84
30
  runActionLabel?: string;
85
- /**
86
- * The caret moved, or the text under it changed. The host tracks it to show what a run would
87
- * send; the run key does not depend on that tracking having caught up.
88
- */
89
- onCaret?: (caret: SqlCaret) => void;
90
31
  /**
91
32
  * The span to mark as the one a run would send. Null leaves the gutter clean, which is the
92
33
  * right answer while there is nothing to choose between.
@@ -108,35 +49,19 @@ export interface SqlEditorProps {
108
49
  * says so — a deliberate keystroke that changes nothing needs to explain itself.
109
50
  */
110
51
  onFormatRefused?: () => void;
111
- onReady?: (handle: SqlEditorHandle) => void;
112
- /**
113
- * Runs after this component's own mount wiring, with the live editor and the monaco module —
114
- * the escape hatch for host-specific concerns (focus shields, extra actions).
115
- */
116
- onMount?: (instance: editor.IStandaloneCodeEditor, monaco: MonacoModule) => void;
117
- /**
118
- * Extra construction options, merged under this editor's own (`ariaLabel`): the escape hatch
119
- * a host reaches for a construction option no prop names — `overflowWidgetsDomNode`, where
120
- * the page's own layers would otherwise stand over the editor's floating widgets (hover,
121
- * completion, parameter hints) left inside it.
122
- */
123
- options?: editor.IStandaloneEditorConstructionOptions;
124
52
  /**
125
53
  * What the editor's language features answer from — the schema and dialect of the connection
126
- * being edited (`createSqlAssist`). Read through a ref at every request, so the host can swap
127
- * it without re-registering anything with Monaco.
54
+ * being edited (`createSqlAssist`). Read at every request, so the host can swap it without
55
+ * re-registering anything with Monaco.
128
56
  */
129
57
  assist?: SqlAssist;
130
58
  }
131
59
  /**
132
- * The SQL editing surface: Monaco in `sql`, highlighted by the same shared Shiki grammar every
133
- * other inkstone surface uses. It is deliberately not a script editor — a TypeScript language
134
- * service and an ambient profile have no meaning for a vendor's SQL. Its language features are
135
- * the host's: the `assist` knows the catalog and the dialect and this component only wires it to
136
- * Monaco (`registerSqlLanguage`) — it never guesses at SQL itself.
137
- *
138
- * The editor draws no chrome of its own: it is a band of its host's surface, and the separation
139
- * is the host's hairline rules — a border here would dress a pane up as a form input.
60
+ * The SQL editing surface: `<MonacoConsoleEditor>` in `sql`, highlighted by the same shared Shiki
61
+ * grammar every other inkstone surface uses. It is deliberately not a script editor — a
62
+ * TypeScript language service and an ambient profile have no meaning for a vendor's SQL. Its
63
+ * language features are the host's: the `assist` knows the catalog and the dialect and this
64
+ * component only wires it to Monaco (`registerSqlLanguage`) — it never guesses at SQL itself.
140
65
  */
141
- export declare function SqlEditor({ value, onChange, dark, height, fontSize, locale, placeholder, ariaLabel, languageId, loading, failure, onRun, runActionLabel, onCaret, highlight, highlightClassName, onFormatRefused, onReady, onMount, options, assist }: SqlEditorProps): import("react").JSX.Element;
66
+ export declare function SqlEditor({ languageId, runActionLabel, assist, onFormatRefused, onReady, options, ...props }: SqlEditorProps): ReactNode;
142
67
  //#endregion
package/dist/react.js CHANGED
@@ -1,135 +1,46 @@
1
1
  import { t as registerSqlLanguage } from "./monaco-DEMsSuzD.js";
2
- import { MonacoEditor } from "@coldsmirk/inkstone-react/monaco";
3
- import { useEffect, useInsertionEffect, useRef, useState } from "react";
2
+ import { MonacoConsoleEditor } from "@coldsmirk/inkstone-react/monaco";
3
+ import { useMemo, useRef } from "react";
4
4
  import { jsx } from "react/jsx-runtime";
5
5
  //#region src/react.tsx
6
- const EDITOR_ID_KEY = Symbol.for("coldsmirk.inkstone.sql.editorId");
7
- function nextEditorId() {
8
- const holder = globalThis;
9
- const id = holder[EDITOR_ID_KEY] ?? 0;
10
- holder[EDITOR_ID_KEY] = id + 1;
11
- return id;
12
- }
13
6
  const FORMAT_CHORD_CONTEXT_KEY = "inkstoneSqlFormatChord";
14
- function useLatest(value) {
15
- const ref = useRef(value);
16
- useInsertionEffect(() => {
17
- ref.current = value;
18
- });
19
- return ref;
20
- }
21
- function caretOf(instance) {
22
- const model = instance.getModel();
23
- const selection = instance.getSelection();
24
- if (model === null || selection === null) return {
25
- text: "",
26
- from: 0,
27
- to: 0
28
- };
29
- return {
30
- text: model.getValue(),
31
- from: model.getOffsetAt(selection.getStartPosition()),
32
- to: model.getOffsetAt(selection.getEndPosition())
33
- };
34
- }
35
- function monacoRange(model, span) {
36
- const from = model.getPositionAt(span.from);
37
- const to = model.getPositionAt(span.to);
38
- return {
39
- startLineNumber: from.lineNumber,
40
- startColumn: from.column,
41
- endLineNumber: to.lineNumber,
42
- endColumn: to.column
43
- };
44
- }
45
- function SqlEditor({ value, onChange, dark = false, height = 200, fontSize, locale, placeholder, ariaLabel, languageId = "sql", loading, failure, onRun, runActionLabel = "Run statement", onCaret, highlight = null, highlightClassName = "inkstone-sql-runnable", onFormatRefused, onReady, onMount, options, assist }) {
46
- const [modelPath] = useState(() => `file:///inkstone-sql/${nextEditorId()}.sql`);
47
- const bindings = useRef([]);
48
- const assistRef = useLatest(assist ?? null);
49
- const formatRefusedRef = useLatest(onFormatRefused);
50
- const runRef = useLatest(onRun);
51
- const caretChangedRef = useLatest(onCaret);
52
- const readyRef = useLatest(onReady);
53
- const mountRef = useLatest(onMount);
54
- const instance = useRef(null);
55
- const marked = useRef(null);
56
- const [mounted, setMounted] = useState(false);
57
- useEffect(() => {
58
- const model = instance.current?.getModel();
59
- if (model === null || model === void 0) return;
60
- marked.current?.clear();
61
- marked.current = highlight === null ? null : instance.current.createDecorationsCollection([{
62
- range: monacoRange(model, highlight),
63
- options: {
64
- linesDecorationsClassName: highlightClassName,
65
- isWholeLine: true
66
- }
67
- }]);
68
- }, [
69
- highlight,
70
- highlightClassName,
71
- mounted
72
- ]);
73
- useEffect(() => () => {
74
- for (const binding of bindings.current) binding.dispose();
75
- bindings.current = [];
76
- }, []);
77
- const fill = height === "fill";
78
- return /* @__PURE__ */ jsx("div", {
79
- style: fill ? {
80
- height: "100%",
81
- minHeight: 0
82
- } : { height },
83
- children: /* @__PURE__ */ jsx(MonacoEditor, {
84
- showLineNumbers: true,
85
- dark,
86
- failure,
87
- fontSize,
88
- height: fill ? "100%" : height,
89
- language: languageId,
90
- loading,
91
- locale,
92
- path: modelPath,
93
- placeholder,
94
- value,
95
- options: {
96
- ...options,
97
- ariaLabel,
98
- suggest: { snippetsPreventQuickSuggestions: false }
99
- },
100
- onChange,
101
- onMount: (live, monaco) => {
102
- instance.current = live;
103
- live.createContextKey(FORMAT_CHORD_CONTEXT_KEY, true);
104
- bindings.current.push(live.onDidChangeCursorSelection(() => caretChangedRef.current?.(caretOf(live))), live.addAction({
105
- id: "inkstone-sql.run",
106
- label: runActionLabel,
107
- keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
108
- run: (current) => runRef.current?.(caretOf(current))
109
- }), monaco.editor.addKeybindingRule({
110
- keybinding: monaco.KeyMod.Shift | monaco.KeyMod.Alt | monaco.KeyCode.KeyF,
111
- command: "editor.action.formatDocument",
112
- when: `${FORMAT_CHORD_CONTEXT_KEY} && editorTextFocus && !editorReadonly`
113
- }), ...registerSqlLanguage(monaco, modelPath, {
114
- assist: () => assistRef.current,
115
- onFormatRefused: () => formatRefusedRef.current?.()
116
- }, languageId));
117
- readyRef.current?.({
118
- format: () => void live.getAction("editor.action.formatDocument")?.run(),
119
- insert: (text) => {
120
- const selection = live.getSelection();
121
- if (selection) live.executeEdits("inkstone-sql-insert", [{
122
- range: selection,
123
- text,
124
- forceMoveMarkers: true
125
- }]);
126
- live.focus();
127
- }
128
- });
129
- mountRef.current?.(live, monaco);
130
- setMounted(true);
131
- }
132
- })
7
+ function SqlEditor({ languageId = "sql", runActionLabel = "Run statement", assist, onFormatRefused, onReady, options, ...props }) {
8
+ const live = useRef(null);
9
+ const sqlOptions = useMemo(() => {
10
+ return {
11
+ ...options,
12
+ suggest: { snippetsPreventQuickSuggestions: false }
13
+ };
14
+ }, [options]);
15
+ return /* @__PURE__ */ jsx(MonacoConsoleEditor, {
16
+ ...props,
17
+ context: {
18
+ assist,
19
+ onFormatRefused
20
+ },
21
+ extension: "sql",
22
+ language: languageId,
23
+ namespace: "inkstone-sql",
24
+ options: sqlOptions,
25
+ runActionLabel,
26
+ register: ({ monaco, editor: instance, path, context }) => {
27
+ live.current = instance;
28
+ instance.createContextKey(FORMAT_CHORD_CONTEXT_KEY, true);
29
+ return [monaco.editor.addKeybindingRule({
30
+ keybinding: monaco.KeyMod.Shift | monaco.KeyMod.Alt | monaco.KeyCode.KeyF,
31
+ command: "editor.action.formatDocument",
32
+ when: `${FORMAT_CHORD_CONTEXT_KEY} && editorTextFocus && !editorReadonly`
33
+ }), ...registerSqlLanguage(monaco, path, {
34
+ assist: () => context().assist ?? null,
35
+ onFormatRefused: () => context().onFormatRefused?.()
36
+ }, languageId)];
37
+ },
38
+ onReady: (handle) => {
39
+ onReady?.({
40
+ ...handle,
41
+ format: () => void live.current?.getAction("editor.action.formatDocument")?.run()
42
+ });
43
+ }
133
44
  });
134
45
  }
135
46
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coldsmirk/inkstone-sql",
3
- "version": "0.31.0",
3
+ "version": "0.33.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.31.0"
51
+ "@coldsmirk/inkstone-react": "^0.33.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "monaco-editor": "^0.57.0",
55
55
  "react": ">=19",
56
- "@coldsmirk/inkstone-react": "^0.31.0"
56
+ "@coldsmirk/inkstone-react": "^0.33.0"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "@coldsmirk/inkstone-react": {