@coldsmirk/inkstone-sql 0.33.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
@@ -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) (`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.
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
 
package/dist/index.d.ts CHANGED
@@ -152,9 +152,10 @@ export declare function joinGuesses(kind: SqlKind, target: SqlSource, others: Sq
152
152
  * has no placeholder in it. Blanking is the reader's job because the reader already owns what is
153
153
  * prose and what is SQL for this page, and it takes that decision under the data source's own
154
154
  * lexical profile (`noise.ts`), mode for mode with the scanner — MySQL's `#` comments and
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.
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.
158
159
  */
159
160
  /**
160
161
  * The binding form a statement's markers suggest.
@@ -166,8 +167,8 @@ type MarkerForm = "none" | "positional" | "named";
166
167
  *
167
168
  * - `"mixed-forms"` — the statement writes both `?` and `:name`; the scanner binds neither.
168
169
  * - `"question-escape-unsupported"` — `??` on a kind whose native form is itself `?` (MySQL,
169
- * 达梦, ClickHouse): the escape has no rendering that survives, and none of them has a `?`
170
- * operator to 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.
171
172
  * - `"native-marker-collision"` — a marker hand-written in the kind's own bind form (`$1`,
172
173
  * `:1`, `@p1`) beside a `?`/`:name` the scanner would mint: both would bind the same slot.
173
174
  */
package/dist/index.js CHANGED
@@ -181,7 +181,7 @@ 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 = {
184
+ const CLICKHOUSE$1 = {
185
185
  ...dialect([
186
186
  "final",
187
187
  "prewhere",
@@ -280,7 +280,7 @@ const SQL_DIALECTS = {
280
280
  fn("string_agg", "string_agg(expr, delimiter)", "Aggregate: joins each row's value with a delimiter.")
281
281
  ]),
282
282
  dm: ORACLE$1,
283
- clickhouse: CLICKHOUSE
283
+ clickhouse: CLICKHOUSE$1
284
284
  };
285
285
  function signatureParameters(signature) {
286
286
  const open = signature.indexOf("(");
@@ -494,6 +494,67 @@ function joinGuesses(kind, target, others) {
494
494
  return [...found.values()];
495
495
  }
496
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
497
558
  //#region src/noise.ts
498
559
  const BASE = {
499
560
  backslashEscapes: false,
@@ -505,13 +566,23 @@ const BASE = {
505
566
  dollarQuoting: false,
506
567
  escapeStrings: false,
507
568
  nestedBlockComments: false,
508
- alternativeQuoting: false
569
+ alternativeQuoting: false,
570
+ backslashInBackticks: false,
571
+ conditionalOperator: false
509
572
  };
510
573
  const SQLSERVER = {
511
574
  ...BASE,
512
575
  bracketIdentifiers: true,
513
576
  nestedBlockComments: true
514
577
  };
578
+ const CLICKHOUSE = {
579
+ ...BASE,
580
+ backslashEscapes: true,
581
+ hashLineComments: true,
582
+ backtickIdentifiers: true,
583
+ nestedBlockComments: true,
584
+ backslashInBackticks: true
585
+ };
515
586
  const NOISE_PROFILES = {
516
587
  postgres: {
517
588
  ...BASE,
@@ -547,17 +618,18 @@ const NOISE_PROFILES = {
547
618
  nestedBlockComments: true,
548
619
  dollarQuoting: true
549
620
  },
621
+ clickhouse: CLICKHOUSE
622
+ };
623
+ const DRIVER_PROFILES = {
624
+ sqlserver: {
625
+ ...SQLSERVER,
626
+ nestedBlockComments: false
627
+ },
550
628
  clickhouse: {
551
- ...BASE,
552
- backslashEscapes: true,
553
- hashLineComments: true,
554
- backtickIdentifiers: true
629
+ ...CLICKHOUSE,
630
+ conditionalOperator: true
555
631
  }
556
632
  };
557
- const DRIVER_PROFILES = { sqlserver: {
558
- ...SQLSERVER,
559
- nestedBlockComments: false
560
- } };
561
633
  const WORD_START = /[a-z_\u{0080}-\u{FFFF}]/iu;
562
634
  const WORD_BYTE = /[\w$\u{0080}-\u{FFFF}]/u;
563
635
  const NAME_BYTE = /\w/;
@@ -689,11 +761,18 @@ function blankNoise(text, kind, lexicon = "database") {
689
761
  const ch = text[at];
690
762
  if (ch === "'") skip(closeDelimited(text, at + 1, "'", profile.backslashEscapes), false);
691
763
  else if (ch === "\"") skip(closeDelimited(text, at + 1, "\"", profile.backslashEscapes), !profile.doubleQuotedStrings);
692
- 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);
693
765
  else if (ch === "[" && profile.bracketIdentifiers) skip(closeBracket(text, at + 1), true);
694
766
  else if (opensLineComment(text, at, profile)) skip(closeLine(text, at), false);
695
767
  else if (ch === "/" && text[at + 1] === "*") skip(closeBlock(text, at + 2, profile.nestedBlockComments), false);
696
- 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) {
697
776
  const quoted = closeDollarQuoted(text, at);
698
777
  if (quoted === null) at += 1;
699
778
  else skip(quoted, false);
@@ -1517,7 +1596,7 @@ function createSqlAssist(kind, catalog) {
1517
1596
  }
1518
1597
  //#endregion
1519
1598
  //#region src/markers.ts
1520
- const SCAN = /[A-Z_\u0080-\uFFFF][\w$\u0080-\uFFFF]*|(?<qq>\?\?)|::|(?<q>\?)|:(?<name>[A-Z_]\w*)|(?<colonDigits>:\d+)|(?<dollarDigits>\$\d+)|(?<atDigits>@P\d+)/gi;
1599
+ const SCAN = /[A-Z_\u0080-\uFFFF][\w$\u0080-\uFFFF]*|(?<qq>\?\?)|::|(?<q>\?)|(?<!\.):(?<name>[A-Z_]\w*)|(?<colonDigits>:\d+)|(?<dollarDigits>\$\d+)|(?<atDigits>@P\d+)/gi;
1521
1600
  const QUESTION_IS_NATIVE = /* @__PURE__ */ new Set([
1522
1601
  "mysql",
1523
1602
  "dm",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coldsmirk/inkstone-sql",
3
- "version": "0.33.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.33.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.33.0"
56
+ "@coldsmirk/inkstone-react": "^0.34.0"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "@coldsmirk/inkstone-react": {