@intentius/chant-lexicon-sql 0.124.12 → 0.124.13

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 (148) hide show
  1. package/dist/clickhouse/apply/statements.d.ts.map +1 -1
  2. package/dist/clickhouse/entities.d.ts +7 -1
  3. package/dist/clickhouse/entities.d.ts.map +1 -1
  4. package/dist/clickhouse/expressions.d.ts +27 -0
  5. package/dist/clickhouse/expressions.d.ts.map +1 -0
  6. package/dist/clickhouse/parser.d.ts +12 -2
  7. package/dist/clickhouse/parser.d.ts.map +1 -1
  8. package/dist/clickhouse/plan/diff.d.ts.map +1 -1
  9. package/dist/clickhouse/plan/normalize.d.ts +11 -1
  10. package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
  11. package/dist/clickhouse/plan/rebuild-handoff.d.ts.map +1 -1
  12. package/dist/clickhouse/plan/rules.d.ts +1 -0
  13. package/dist/clickhouse/plan/rules.d.ts.map +1 -1
  14. package/dist/clickhouse.d.ts +1 -0
  15. package/dist/clickhouse.d.ts.map +1 -1
  16. package/dist/codegen/classifier-page.d.ts.map +1 -1
  17. package/dist/codegen/postgres-classifier-page.d.ts.map +1 -1
  18. package/dist/core/cursor.d.ts +41 -0
  19. package/dist/core/cursor.d.ts.map +1 -1
  20. package/dist/core/interpolation.d.ts +18 -1
  21. package/dist/core/interpolation.d.ts.map +1 -1
  22. package/dist/files/clickhouse.d.ts +2 -2
  23. package/dist/files/clickhouse.d.ts.map +1 -1
  24. package/dist/files/common.d.ts +16 -0
  25. package/dist/files/common.d.ts.map +1 -1
  26. package/dist/files/postgres.d.ts +2 -2
  27. package/dist/files/postgres.d.ts.map +1 -1
  28. package/dist/import-parser.d.ts +2 -1
  29. package/dist/import-parser.d.ts.map +1 -1
  30. package/dist/integrity.json +19 -19
  31. package/dist/lint/post-synth/clickhouse-helpers.d.ts +6 -19
  32. package/dist/lint/post-synth/clickhouse-helpers.d.ts.map +1 -1
  33. package/dist/lint/post-synth/postgres-helpers.d.ts +15 -55
  34. package/dist/lint/post-synth/postgres-helpers.d.ts.map +1 -1
  35. package/dist/lint/post-synth/postgres-names.d.ts +2 -2
  36. package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
  37. package/dist/lint/post-synth/sql-helpers.d.ts +5 -6
  38. package/dist/lint/post-synth/sql-helpers.d.ts.map +1 -1
  39. package/dist/lint/post-synth/sqlch102.d.ts.map +1 -1
  40. package/dist/lint/post-synth/sqlch103.d.ts.map +1 -1
  41. package/dist/lint/post-synth/sqlch104.d.ts.map +1 -1
  42. package/dist/lint/post-synth/sqlch105.d.ts.map +1 -1
  43. package/dist/lint/post-synth/sqlch106.d.ts.map +1 -1
  44. package/dist/lint/post-synth/sqlch107.d.ts.map +1 -1
  45. package/dist/lint/post-synth/sqlch111.d.ts.map +1 -1
  46. package/dist/lint/post-synth/sqlch112.d.ts.map +1 -1
  47. package/dist/lint/post-synth/sqlch113.d.ts.map +1 -1
  48. package/dist/lint/post-synth/sqlch114.d.ts.map +1 -1
  49. package/dist/lint/post-synth/sqlch115.d.ts.map +1 -1
  50. package/dist/lint/post-synth/sqlch120.d.ts.map +1 -1
  51. package/dist/lint/post-synth/sqlch125.d.ts.map +1 -1
  52. package/dist/manifest.json +1 -1
  53. package/dist/postgres/apply/statements.d.ts.map +1 -1
  54. package/dist/postgres/entities.d.ts +7 -1
  55. package/dist/postgres/entities.d.ts.map +1 -1
  56. package/dist/postgres/parser.d.ts +10 -2
  57. package/dist/postgres/parser.d.ts.map +1 -1
  58. package/dist/postgres/plan/diff.d.ts.map +1 -1
  59. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  60. package/dist/postgres/plan/rules.d.ts +1 -0
  61. package/dist/postgres/plan/rules.d.ts.map +1 -1
  62. package/dist/postgres.d.ts +1 -0
  63. package/dist/postgres.d.ts.map +1 -1
  64. package/dist/read/clickhouse.d.ts +66 -0
  65. package/dist/read/clickhouse.d.ts.map +1 -0
  66. package/dist/read/objects.d.ts +99 -0
  67. package/dist/read/objects.d.ts.map +1 -0
  68. package/dist/read/postgres.d.ts +83 -0
  69. package/dist/read/postgres.d.ts.map +1 -0
  70. package/dist/read.d.ts +49 -0
  71. package/dist/read.d.ts.map +1 -0
  72. package/dist/rules/clickhouse-helpers.ts +20 -97
  73. package/dist/rules/postgres-helpers.ts +18 -53
  74. package/dist/rules/postgres-names.ts +6 -6
  75. package/dist/rules/sql-helpers.ts +8 -28
  76. package/dist/rules/sqlch102.ts +2 -3
  77. package/dist/rules/sqlch103.ts +2 -3
  78. package/dist/rules/sqlch104.ts +2 -3
  79. package/dist/rules/sqlch105.ts +2 -3
  80. package/dist/rules/sqlch106.ts +2 -3
  81. package/dist/rules/sqlch107.ts +2 -3
  82. package/dist/rules/sqlch111.ts +2 -3
  83. package/dist/rules/sqlch112.ts +2 -3
  84. package/dist/rules/sqlch113.ts +2 -3
  85. package/dist/rules/sqlch114.ts +2 -3
  86. package/dist/rules/sqlch115.ts +2 -3
  87. package/dist/rules/sqlch120.ts +2 -3
  88. package/dist/rules/sqlch125.ts +2 -3
  89. package/dist/serializer.d.ts +11 -1
  90. package/dist/serializer.d.ts.map +1 -1
  91. package/dist/sql-files.d.ts +17 -1
  92. package/dist/sql-files.d.ts.map +1 -1
  93. package/dist/sql-types.d.ts +3 -0
  94. package/dist/sql-types.d.ts.map +1 -1
  95. package/package.json +2 -2
  96. package/src/clickhouse/apply/statements.ts +2 -0
  97. package/src/clickhouse/entities.ts +8 -1
  98. package/src/clickhouse/expressions.ts +108 -0
  99. package/src/clickhouse/parser.ts +113 -31
  100. package/src/clickhouse/plan/diff.ts +19 -0
  101. package/src/clickhouse/plan/normalize.ts +27 -2
  102. package/src/clickhouse/plan/rebuild-handoff.ts +2 -1
  103. package/src/clickhouse/plan/rules.ts +7 -0
  104. package/src/clickhouse/testdata/server-ddl-baseline.json +67 -0
  105. package/src/clickhouse.ts +10 -0
  106. package/src/codegen/classifier-page.ts +2 -0
  107. package/src/codegen/postgres-classifier-page.ts +2 -0
  108. package/src/core/cursor.ts +96 -0
  109. package/src/core/interpolation.ts +32 -1
  110. package/src/files/clickhouse.ts +7 -5
  111. package/src/files/common.ts +23 -0
  112. package/src/files/postgres.ts +12 -8
  113. package/src/import-parser.ts +2 -1
  114. package/src/lint/post-synth/clickhouse-helpers.ts +20 -97
  115. package/src/lint/post-synth/postgres-helpers.ts +18 -53
  116. package/src/lint/post-synth/postgres-names.ts +6 -6
  117. package/src/lint/post-synth/sql-helpers.ts +8 -28
  118. package/src/lint/post-synth/sqlch102.ts +2 -3
  119. package/src/lint/post-synth/sqlch103.ts +2 -3
  120. package/src/lint/post-synth/sqlch104.ts +2 -3
  121. package/src/lint/post-synth/sqlch105.ts +2 -3
  122. package/src/lint/post-synth/sqlch106.ts +2 -3
  123. package/src/lint/post-synth/sqlch107.ts +2 -3
  124. package/src/lint/post-synth/sqlch111.ts +2 -3
  125. package/src/lint/post-synth/sqlch112.ts +2 -3
  126. package/src/lint/post-synth/sqlch113.ts +2 -3
  127. package/src/lint/post-synth/sqlch114.ts +2 -3
  128. package/src/lint/post-synth/sqlch115.ts +2 -3
  129. package/src/lint/post-synth/sqlch120.ts +2 -3
  130. package/src/lint/post-synth/sqlch125.ts +2 -3
  131. package/src/postgres/apply/statements.ts +2 -0
  132. package/src/postgres/entities.ts +9 -1
  133. package/src/postgres/parser.ts +31 -8
  134. package/src/postgres/plan/diff.ts +18 -0
  135. package/src/postgres/plan/normalize.ts +12 -0
  136. package/src/postgres/plan/rules.ts +7 -0
  137. package/src/postgres/testdata/server-ddl-baseline.json +38 -0
  138. package/src/postgres.ts +12 -0
  139. package/src/read/clickhouse.ts +137 -0
  140. package/src/read/objects.ts +137 -0
  141. package/src/read/postgres.ts +180 -0
  142. package/src/read-policies.e2e.test.ts +144 -0
  143. package/src/read.test.ts +143 -0
  144. package/src/read.ts +69 -0
  145. package/src/serializer.ts +37 -1
  146. package/src/sql-files.ts +27 -6
  147. package/src/sql-types.ts +19 -2
  148. package/src/unknown-clauses.test.ts +227 -0
@@ -1,41 +1,26 @@
1
1
  /**
2
2
  * Reading the sql lexicon's build output in a post-synth check: the JSON
3
- * document the serializer writes (`dialect`, `applyOrder`, `objects`).
3
+ * document the serializer writes (`dialect`, `applyOrder`, `objects`). The
4
+ * documents are found by `../../read/objects.ts`, the same path the public
5
+ * read API (`@intentius/chant-lexicon-sql/read`) takes.
4
6
  */
5
7
 
6
8
  import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
9
+ import { dialectObjects, recordedPostgresMajor, type SqlObjectJson } from "../../read/objects";
7
10
 
8
- export interface OutputObject {
9
- export: string;
10
- type: string;
11
- name: string;
11
+ export interface OutputObject extends SqlObjectJson {
12
12
  database?: string;
13
13
  engine?: { name: string; args?: string[] };
14
- [key: string]: unknown;
15
14
  }
16
15
 
17
16
  /** Every schema object in the build's sql output, ClickHouse dialect. */
18
17
  export function clickhouseObjects(ctx: PostSynthContext): OutputObject[] {
19
- const out: OutputObject[] = [];
20
- for (const doc of ctx.docs ?? []) {
21
- if (doc.error || doc.lexicon !== "sql" || doc.format !== "json") continue;
22
- const value = doc.value as { dialect?: unknown; objects?: unknown };
23
- if (value?.dialect !== "clickhouse" || !Array.isArray(value.objects)) continue;
24
- out.push(...(value.objects as OutputObject[]));
25
- }
26
- return out;
18
+ return dialectObjects(ctx, "clickhouse") as OutputObject[];
27
19
  }
28
20
 
29
21
  /** Every schema object in the build's sql output, Postgres dialect. */
30
22
  export function postgresObjects(ctx: PostSynthContext): OutputObject[] {
31
- const out: OutputObject[] = [];
32
- for (const doc of ctx.docs ?? []) {
33
- if (doc.error || doc.lexicon !== "sql" || doc.format !== "json") continue;
34
- const value = doc.value as { dialect?: unknown; objects?: unknown };
35
- if (value?.dialect !== "postgres" || !Array.isArray(value.objects)) continue;
36
- out.push(...(value.objects as OutputObject[]));
37
- }
38
- return out;
23
+ return dialectObjects(ctx, "postgres") as OutputObject[];
39
24
  }
40
25
 
41
26
  /**
@@ -44,10 +29,5 @@ export function postgresObjects(ctx: PostSynthContext): OutputObject[] {
44
29
  * output that carries none.
45
30
  */
46
31
  export function postgresMajorOf(ctx: PostSynthContext, fallback: number): number {
47
- for (const doc of ctx.docs ?? []) {
48
- if (doc.error || doc.lexicon !== "sql" || doc.format !== "json") continue;
49
- const value = doc.value as { dialect?: unknown; postgresMajor?: unknown };
50
- if (value?.dialect === "postgres" && typeof value.postgresMajor === "number") return value.postgresMajor;
51
- }
52
- return fallback;
32
+ return recordedPostgresMajor(ctx, fallback);
53
33
  }
@@ -1,5 +1,4 @@
1
- import { checkOf, isTable, keyElements } from "./clickhouse-helpers";
2
- import { clickhouseObjects } from "./sql-helpers";
1
+ import { checkOf, keyElements, tablesOf } from "./clickhouse-helpers";
3
2
 
4
3
  /**
5
4
  * SQLCH102: a PRIMARY KEY that is not a prefix of ORDER BY.
@@ -9,7 +8,7 @@ import { clickhouseObjects } from "./sql-helpers";
9
8
  * the table at CREATE.
10
9
  */
11
10
  export const sqlch102 = checkOf({ id: "SQLCH102", description: "A PRIMARY KEY is not a prefix of ORDER BY" }, (ctx, report) => {
12
- for (const t of clickhouseObjects(ctx).filter(isTable)) {
11
+ for (const t of tablesOf(ctx)) {
13
12
  if (!t.primaryKey || !t.orderBy) continue;
14
13
  const pk = keyElements(t.primaryKey).map((e) => e.replace(/\s+/g, ""));
15
14
  const ob = keyElements(t.orderBy).map((e) => e.replace(/\s+/g, ""));
@@ -1,5 +1,4 @@
1
- import { baseEngineName, checkOf, columnByName, engineArguments, isNullableType, isTable, typeFamily } from "./clickhouse-helpers";
2
- import { clickhouseObjects } from "./sql-helpers";
1
+ import { baseEngineName, checkOf, columnByName, engineArguments, isNullableType, tablesOf, typeFamily } from "./clickhouse-helpers";
3
2
 
4
3
  const VERSION_TYPES = new Set(["UInt8", "UInt16", "UInt32", "UInt64", "UInt128", "UInt256", "Date", "DateTime", "DateTime64"]);
5
4
 
@@ -25,7 +24,7 @@ const SPECIAL: Record<string, Array<{ role: string; index: number; accepts: (fam
25
24
  * .../collapsingmergetree, .../versionedcollapsingmergetree (sign: Int8).
26
25
  */
27
26
  export const sqlch103 = checkOf({ id: "SQLCH103", description: "An engine's version, sign or is_deleted column has an unsupported type" }, (ctx, report) => {
28
- for (const t of clickhouseObjects(ctx).filter(isTable)) {
27
+ for (const t of tablesOf(ctx)) {
29
28
  if (!t.engine) continue;
30
29
  const roles = SPECIAL[baseEngineName(t.engine)];
31
30
  if (!roles) continue;
@@ -1,5 +1,4 @@
1
- import { baseEngineName, checkOf, engineArguments, isTable, splitTop } from "./clickhouse-helpers";
2
- import { clickhouseObjects } from "./sql-helpers";
1
+ import { baseEngineName, checkOf, engineArguments, splitTop, tablesOf } from "./clickhouse-helpers";
3
2
 
4
3
  /** The engine arguments that name columns, by base engine. */
5
4
  function columnArguments(engine: string, args: string[]): string[] {
@@ -26,7 +25,7 @@ function columnArguments(engine: string, args: string[]): string[] {
26
25
  * sign, version, is_deleted and summed columns are columns of the table).
27
26
  */
28
27
  export const sqlch104 = checkOf({ id: "SQLCH104", description: "A MergeTree engine argument names a column the table does not declare" }, (ctx, report) => {
29
- for (const t of clickhouseObjects(ctx).filter(isTable)) {
28
+ for (const t of tablesOf(ctx)) {
30
29
  if (!t.engine || t.columns.length === 0) continue;
31
30
  const declared = new Set(t.columns.map((c) => c.name));
32
31
  for (const arg of columnArguments(baseEngineName(t.engine), engineArguments(t.engine))) {
@@ -1,5 +1,4 @@
1
- import { bareName, checkOf, isTable, keyElements } from "./clickhouse-helpers";
2
- import { clickhouseObjects } from "./sql-helpers";
1
+ import { bareName, checkOf, keyElements, tablesOf } from "./clickhouse-helpers";
3
2
 
4
3
  /**
5
4
  * SQLCH105: ORDER BY, PRIMARY KEY, PARTITION BY or SAMPLE BY lists a bare
@@ -10,7 +9,7 @@ import { clickhouseObjects } from "./sql-helpers";
10
9
  * judged; an expression is left to the server.
11
10
  */
12
11
  export const sqlch105 = checkOf({ id: "SQLCH105", description: "A table key clause names a column the table does not declare" }, (ctx, report) => {
13
- for (const t of clickhouseObjects(ctx).filter(isTable)) {
12
+ for (const t of tablesOf(ctx)) {
14
13
  if (t.columns.length === 0) continue;
15
14
  const declared = new Set(t.columns.map((c) => c.name));
16
15
  const clauses: Array<[string, string | undefined]> = [
@@ -1,5 +1,4 @@
1
- import { checkOf, isTable, mentionedNames } from "./clickhouse-helpers";
2
- import { clickhouseObjects } from "./sql-helpers";
1
+ import { checkOf, mentionedNames, tablesOf } from "./clickhouse-helpers";
3
2
 
4
3
  /**
5
4
  * SQLCH106: a data skipping index expression mentions a column the table does
@@ -11,7 +10,7 @@ import { clickhouseObjects } from "./sql-helpers";
11
10
  * SQL parser would guess.
12
11
  */
13
12
  export const sqlch106 = checkOf({ id: "SQLCH106", description: "A skip index expression names a column the table does not declare" }, (ctx, report) => {
14
- for (const t of clickhouseObjects(ctx).filter(isTable)) {
13
+ for (const t of tablesOf(ctx)) {
15
14
  if (t.columns.length === 0) continue;
16
15
  const declared = new Set(t.columns.map((c) => c.name));
17
16
  for (const index of t.indexes ?? []) {
@@ -1,5 +1,4 @@
1
- import { bareName, checkOf, isTable, splitTop, typeFamily } from "./clickhouse-helpers";
2
- import { clickhouseObjects } from "./sql-helpers";
1
+ import { bareName, checkOf, splitTop, tablesOf, typeFamily } from "./clickhouse-helpers";
3
2
 
4
3
  const DATE_TYPES = new Set(["Date", "Date32", "DateTime", "DateTime64"]);
5
4
  const TTL_ACTION = /\s+(DELETE|TO\s+DISK|TO\s+VOLUME|RECOMPRESS|GROUP\s+BY|WHERE)\b[\s\S]*$/i;
@@ -13,7 +12,7 @@ const TTL_ACTION = /\s+(DELETE|TO\s+DISK|TO\s+VOLUME|RECOMPRESS|GROUP\s+BY|WHERE
13
12
  * a function call is left to the server.
14
13
  */
15
14
  export const sqlch107 = checkOf({ id: "SQLCH107", description: "A TTL expression is built on a column that is not a Date or DateTime" }, (ctx, report) => {
16
- for (const t of clickhouseObjects(ctx).filter(isTable)) {
15
+ for (const t of tablesOf(ctx)) {
17
16
  const ttls: Array<{ where: string; expr: string }> = [];
18
17
  if (t.ttl) for (const clause of splitTop(t.ttl)) ttls.push({ where: "table TTL", expr: clause.replace(TTL_ACTION, "") });
19
18
  for (const c of t.columns) if (c.ttl) ttls.push({ where: `column ${c.name} TTL`, expr: c.ttl.replace(TTL_ACTION, "") });
@@ -1,5 +1,4 @@
1
- import { checkOf, isTable, splitTop } from "./clickhouse-helpers";
2
- import { clickhouseObjects } from "./sql-helpers";
1
+ import { checkOf, splitTop, tablesOf } from "./clickhouse-helpers";
3
2
 
4
3
  const MAX_VALUES = 100;
5
4
 
@@ -23,7 +22,7 @@ function allowedValues(expr: string): { column: string; count: number } | undefi
23
22
  * https://clickhouse.com/docs/best-practices/select-data-types.
24
23
  */
25
24
  export const sqlch111 = checkOf({ id: "SQLCH111", description: "A String column limited to a few values is not LowCardinality" }, (ctx, report) => {
26
- for (const t of clickhouseObjects(ctx).filter(isTable)) {
25
+ for (const t of tablesOf(ctx)) {
27
26
  for (const c of t.constraints ?? []) {
28
27
  if (c.kind !== "CHECK") continue;
29
28
  const found = allowedValues(c.expr);
@@ -1,5 +1,4 @@
1
- import { checkOf, isTable, typeFamily } from "./clickhouse-helpers";
2
- import { clickhouseObjects } from "./sql-helpers";
1
+ import { checkOf, tablesOf, typeFamily } from "./clickhouse-helpers";
3
2
 
4
3
  const FINE_FUNCTIONS =
5
4
  /\b(toStartOfSecond|toStartOfMinute|toStartOfFiveMinutes?|toStartOfTenMinutes|toStartOfFifteenMinutes|toStartOfHour|toRelativeSecondNum|toRelativeMinuteNum|toRelativeHourNum|toUnixTimestamp|toYYYYMMDDhhmmss|toStartOfInterval)\s*\(/;
@@ -15,7 +14,7 @@ const FINE_FUNCTIONS =
15
14
  * the sub-day truncation functions.
16
15
  */
17
16
  export const sqlch112 = checkOf({ id: "SQLCH112", description: "A PARTITION BY expression is finer than a day" }, (ctx, report) => {
18
- for (const t of clickhouseObjects(ctx).filter(isTable)) {
17
+ for (const t of tablesOf(ctx)) {
19
18
  const expr = t.partitionBy?.trim();
20
19
  if (!expr) continue;
21
20
  const col = t.columns.find((c) => c.name === expr.replace(/^\(|\)$/g, "").trim());
@@ -1,5 +1,4 @@
1
- import { checkOf, isMergeTree, isTable } from "./clickhouse-helpers";
2
- import { clickhouseObjects } from "./sql-helpers";
1
+ import { checkOf, isMergeTree, tablesOf } from "./clickhouse-helpers";
3
2
 
4
3
  /**
5
4
  * SQLCH113: a MergeTree table with neither ORDER BY nor PRIMARY KEY.
@@ -9,7 +8,7 @@ import { clickhouseObjects } from "./sql-helpers";
9
8
  * `ORDER BY tuple()` to have none). The server refuses the CREATE.
10
9
  */
11
10
  export const sqlch113 = checkOf({ id: "SQLCH113", description: "A MergeTree table declares no sort key" }, (ctx, report) => {
12
- for (const t of clickhouseObjects(ctx).filter(isTable)) {
11
+ for (const t of tablesOf(ctx)) {
13
12
  if (!isMergeTree(t.engine) || t.orderBy || t.primaryKey) continue;
14
13
  report({
15
14
  severity: "error",
@@ -1,6 +1,5 @@
1
1
  import { CODECS } from "../../generated/clickhouse";
2
- import { checkOf, codecNames, isTable } from "./clickhouse-helpers";
3
- import { clickhouseObjects } from "./sql-helpers";
2
+ import { checkOf, codecNames, tablesOf } from "./clickhouse-helpers";
4
3
 
5
4
  /**
6
5
  * SQLCH114: a column CODEC names a codec the pinned server does not have.
@@ -10,7 +9,7 @@ import { clickhouseObjects } from "./sql-helpers";
10
9
  */
11
10
  export const sqlch114 = checkOf({ id: "SQLCH114", description: "A column codec is not one the pinned server has" }, (ctx, report) => {
12
11
  const known = new Set(Object.keys(CODECS).map((n) => n.toLowerCase()));
13
- for (const t of clickhouseObjects(ctx).filter(isTable)) {
12
+ for (const t of tablesOf(ctx)) {
14
13
  for (const c of t.columns) {
15
14
  if (!c.codec) continue;
16
15
  for (const name of codecNames(c.codec)) {
@@ -1,6 +1,5 @@
1
1
  import { CODECS } from "../../generated/clickhouse";
2
- import { checkOf, codecNames, isTable } from "./clickhouse-helpers";
3
- import { clickhouseObjects } from "./sql-helpers";
2
+ import { checkOf, codecNames, tablesOf } from "./clickhouse-helpers";
4
3
 
5
4
  const SENSITIVE = /(^|_)(password|passwd|pwd|secret|api_?key|access_?key|private_?key|token|ssn|social_?security|credit_?card|card_?number|iban)(_|$)/i;
6
5
 
@@ -17,7 +16,7 @@ const SENSITIVE = /(^|_)(password|passwd|pwd|secret|api_?key|access_?key|private
17
16
  */
18
17
  export const sqlch115 = checkOf({ id: "SQLCH115", description: "A secret- or PII-named column carries no comment, TTL or encryption" }, (ctx, report) => {
19
18
  const encryption = new Set(Object.entries(CODECS).filter(([, s]) => s.encryption).map(([n]) => n.toLowerCase()));
20
- for (const t of clickhouseObjects(ctx).filter(isTable)) {
19
+ for (const t of tablesOf(ctx)) {
21
20
  for (const c of t.columns) {
22
21
  if (!SENSITIVE.test(c.name) || c.comment || c.ttl || t.ttl) continue;
23
22
  if (c.codec && codecNames(c.codec).some((n) => encryption.has(n.toLowerCase()))) continue;
@@ -1,5 +1,4 @@
1
- import { checkOf, isTable } from "./clickhouse-helpers";
2
- import { clickhouseObjects } from "./sql-helpers";
1
+ import { checkOf, tablesOf } from "./clickhouse-helpers";
3
2
 
4
3
  /**
5
4
  * SQLCH120: a MergeTree engine written with the legacy positional arguments.
@@ -10,7 +9,7 @@ import { clickhouseObjects } from "./sql-helpers";
10
9
  * syntax is a bare `MergeTree` with ORDER BY, PARTITION BY and SETTINGS.
11
10
  */
12
11
  export const sqlch120 = checkOf({ id: "SQLCH120", description: "A MergeTree engine uses the deprecated positional arguments" }, (ctx, report) => {
13
- for (const t of clickhouseObjects(ctx).filter(isTable)) {
12
+ for (const t of tablesOf(ctx)) {
14
13
  if (t.engine?.name !== "MergeTree" || (t.engine.args ?? []).length === 0) continue;
15
14
  report({
16
15
  severity: "warning",
@@ -1,6 +1,5 @@
1
1
  import { CODECS, CLICKHOUSE_VERSION } from "../../generated/clickhouse";
2
- import { checkOf, isTable, splitTop } from "./clickhouse-helpers";
3
- import { clickhouseObjects } from "./sql-helpers";
2
+ import { checkOf, splitTop, tablesOf } from "./clickhouse-helpers";
4
3
 
5
4
  type Param = { name: string; kind: string; optional?: boolean; range?: readonly [number, number]; values?: readonly string[] };
6
5
 
@@ -17,7 +16,7 @@ const NUMBER = /^-?\d+(\.\d+)?$/;
17
16
  */
18
17
  export const sqlch125 = checkOf({ id: "SQLCH125", description: "A codec parameter is outside what the codec takes" }, (ctx, report) => {
19
18
  const codecs = CODECS as Record<string, { parameters: readonly Param[] | "unknown" } | undefined>;
20
- for (const t of clickhouseObjects(ctx).filter(isTable)) {
19
+ for (const t of tablesOf(ctx)) {
21
20
  for (const c of t.columns) {
22
21
  if (!c.codec) continue;
23
22
  for (const part of splitTop(c.codec)) {
@@ -789,6 +789,8 @@ export function refusalDetail(changes: readonly PgChange[], name: string, declar
789
789
  ? `Run it as the expand-and-contract migration Op: ${ops.join(", ")} from @intentius/chant-lexicon-sql/postgres (add the new column, write both, backfill, switch readers, then drop the old).` +
790
790
  (ops.length < changes.length ? " The other changes have no migration Op yet: make them by hand as expand and contract." : "")
791
791
  : "No migration Op makes this change yet: make it by hand as expand and contract (add the new, write both, backfill, move readers, then drop the old).";
792
+ // A clause chant does not read (#3872) is refused for review: what the change takes is not known.
793
+ if (changes.every((c) => c.rule === "SQLPG299")) return `${name} needs review, since chant does not read the clause, so nothing was sent for it. ${lines.join(" ")}`;
792
794
  return `${name} needs expand and contract, which no in-place statement makes, so nothing was sent for it. ${lines.join(" ")} ${instead}`;
793
795
  }
794
796
 
@@ -58,7 +58,7 @@ import { setInterpolationFields } from "@intentius/chant/provenance";
58
58
  import { isTrivia, POSTGRES_LEXICAL, SqlSyntaxError, untokenize, type Token } from "./tokens";
59
59
  import { identValue, parseStatements, type ColumnNode, type CommentNode, type ConstraintNode, type GrantNode, type NameNode, type RowSecurityNode, type Span, type StatementNode } from "./parser";
60
60
  import { keywordCategory, quoteIdent } from "./keywords";
61
- import { feed, lineage, spanText as text, splice as spliceWith, templateSyntaxError, type TemplateCtx, type TemplateDialect } from "../core/interpolation";
61
+ import { feed, lineage, spanText as text, splice as spliceWith, templateSyntaxError, unparsedClauses, type TemplateCtx, type TemplateDialect, type UnparsedClause } from "../core/interpolation";
62
62
  import { SqlObject, isColumnRefOf, isSqlObjectOf, makeSqlEntity } from "../core/entity";
63
63
  import type { LineageEdge } from "../core/references";
64
64
  import { SqlLiteral, SqlTemplateError, describeValue as describe, stripUnset, templateParts } from "../core/template";
@@ -66,6 +66,7 @@ import { setTypedSlots, type ColumnsIn, type Named, type RowIn, type SlotsIn, ty
66
66
 
67
67
  export { SQL_LEXICON } from "../core/entity";
68
68
  export type { LineageEdge } from "../core/references";
69
+ export type { UnparsedClause } from "../core/interpolation";
69
70
  export { SqlLiteral, SqlTemplateError } from "../core/template";
70
71
  export { quoteIdent } from "./keywords";
71
72
  export type { AnyColumn, Column, ColumnTraits, KeyColumn, Named, NotNullColumn, UncheckedColumn, Unnamed } from "../core/typed";
@@ -155,6 +156,11 @@ interface CommonProps {
155
156
  ddl: string;
156
157
  /** The template's raw parts, as written, for a source round trip. */
157
158
  source: { strings: string[] };
159
+ /**
160
+ * Clauses chant's parser does not know (#3872), kept in `ddl` as written
161
+ * and left to the catalog checks and the server. Absent when there are none.
162
+ */
163
+ unparsed?: UnparsedClause[];
158
164
  }
159
165
 
160
166
  export interface SchemaProps extends Omit<CommonProps, "schema"> {
@@ -1182,7 +1188,9 @@ function build(tag: PostgresTag, strings: TemplateStringsArray | readonly string
1182
1188
  const source = { strings: rawParts };
1183
1189
  const dependsOn = [...new Set([...values, ...(joined?.refs ?? [])].filter((v) => isPostgresObject(v) || isColumnRef(v)))];
1184
1190
  const slots: TypedSlot[] = [];
1191
+ const unparsed = unparsedClauses(ctx, nodes.flatMap((n) => ("unknown" in n && n.unknown ? n.unknown : [])));
1185
1192
  const make = <T extends PostgresObject>(type: PostgresEntityType, q: { schema?: string; name: string }, props: object, columnNames?: readonly string[]): T => {
1193
+ if (unparsed) (props as { unparsed?: UnparsedClause[] }).unparsed = unparsed;
1186
1194
  const entity = makeSqlEntity(PostgresObject.prototype as PostgresObject, type, sqlNameOf(q), props, columnNames, dependsOn) as T;
1187
1195
  const fed = ctx.fed.map((paths) => [...paths].sort());
1188
1196
  if (!joined) setInterpolationFields(entity, fed);
@@ -35,11 +35,11 @@
35
35
  * `src/backend/parser/gram.y` at `REL_18_6` where a page is silent.
36
36
  */
37
37
 
38
- import { kw, SqlCursor, type Span } from "../core/cursor";
38
+ import { kw, SqlCursor, type Span, type UnknownClause } from "../core/cursor";
39
39
  import { SqlSyntaxError, type Token } from "../core/tokens";
40
40
  import { keywordCategory } from "./keywords";
41
41
 
42
- export type { Span } from "../core/cursor";
42
+ export type { Span, UnknownClause } from "../core/cursor";
43
43
  export { SqlSyntaxError };
44
44
 
45
45
  /** An identifier as Postgres reads it: an unquoted name folds to lower case, a quoted one is exact. */
@@ -116,6 +116,8 @@ export interface ColumnNode {
116
116
 
117
117
  export interface TableNode {
118
118
  statement: "table";
119
+ /** Clauses the parser does not know (#3872), kept in the DDL as written; absent when there are none. */
120
+ unknown?: UnknownClause[];
119
121
  persistence?: "temporary" | "unlogged";
120
122
  ifNotExists: boolean;
121
123
  name: NameNode;
@@ -141,6 +143,8 @@ export interface IndexElement {
141
143
 
142
144
  export interface IndexNode {
143
145
  statement: "index";
146
+ /** Clauses the parser does not know (#3872), kept in the DDL as written; absent when there are none. */
147
+ unknown?: UnknownClause[];
144
148
  unique: boolean;
145
149
  concurrently: boolean;
146
150
  ifNotExists: boolean;
@@ -158,6 +162,8 @@ export interface IndexNode {
158
162
 
159
163
  export interface ViewNode {
160
164
  statement: "view";
165
+ /** Clauses the parser does not know (#3872), kept in the DDL as written; absent when there are none. */
166
+ unknown?: UnknownClause[];
161
167
  materialized: boolean;
162
168
  orReplace: boolean;
163
169
  temporary: boolean;
@@ -176,6 +182,8 @@ export interface ViewNode {
176
182
 
177
183
  export interface SequenceNode {
178
184
  statement: "sequence";
185
+ /** Clauses the parser does not know (#3872), kept in the DDL as written; absent when there are none. */
186
+ unknown?: UnknownClause[];
179
187
  persistence?: "temporary" | "unlogged";
180
188
  ifNotExists: boolean;
181
189
  name: NameNode;
@@ -366,6 +374,18 @@ const COLUMN_CLAUSE = [
366
374
  "ENFORCED",
367
375
  ];
368
376
 
377
+ /** A column's clause words, and its constraints' attributes: an unknown clause stops at the next (#3872). */
378
+ const COLUMN_WORDS = [...COLUMN_CLAUSE, "REFERENCES", "ON", "MATCH", "NO", "WITH", "INCLUDE", "USING"];
379
+
380
+ /** The clauses after a table's column list. */
381
+ const TABLE_WORDS = ["INHERITS", "PARTITION", "USING", "WITH", "WITHOUT", "ON", "TABLESPACE", "FOR", "DEFAULT"];
382
+
383
+ /** The clauses after an index's elements. */
384
+ const INDEX_WORDS = ["INCLUDE", "NULLS", "WITH", "TABLESPACE", "WHERE"];
385
+
386
+ /** The clauses before a view's AS. */
387
+ const VIEW_WORDS = ["USING", "WITH", "TABLESPACE", "AS"];
388
+
369
389
  /** `LIKE source_table [ like_option ... ]`'s option words. */
370
390
  const LIKE_OPTIONS = ["COMMENTS", "COMPRESSION", "CONSTRAINTS", "DEFAULTS", "GENERATED", "IDENTITY", "INDEXES", "STATISTICS", "STORAGE", "ALL"];
371
391
 
@@ -653,7 +673,7 @@ class PgParser extends SqlCursor {
653
673
  if (this.accept("COMPRESSION")) col.compression = this.expr(COLUMN_CLAUSE);
654
674
  else if (this.accept("STORAGE")) col.storage = this.expr(COLUMN_CLAUSE);
655
675
  else if (this.accept("COLLATE")) col.collate = this.expr(COLUMN_CLAUSE);
656
- else if (!this.columnConstraint(col)) break;
676
+ else if (!this.columnConstraint(col) && !this.unknownClause(COLUMN_WORDS, { inList: true, column: text || undefined })) break;
657
677
  }
658
678
  const after = this.peek();
659
679
  if (after && !(after.kind === "punct" && (after.text === "," || after.text === ")"))) this.fail("expected a column constraint, ',' or ')'");
@@ -991,7 +1011,7 @@ class PgParser extends SqlCursor {
991
1011
  if (!(this.acceptSeq("PRESERVE", "ROWS") || this.acceptSeq("DELETE", "ROWS") || this.accept("DROP"))) this.fail("expected PRESERVE ROWS, DELETE ROWS or DROP");
992
1012
  node.onCommit = this.span(from, []);
993
1013
  } else if (this.accept("TABLESPACE")) node.tablespace = this.nameTok("a tablespace").span;
994
- else break;
1014
+ else if (!this.unknownClause(TABLE_WORDS)) break;
995
1015
  }
996
1016
  return node;
997
1017
  }
@@ -1061,7 +1081,7 @@ class PgParser extends SqlCursor {
1061
1081
  node.with = this.reloptions();
1062
1082
  } else if (this.accept("TABLESPACE")) node.tablespace = this.nameTok("a tablespace").span;
1063
1083
  else if (this.accept("WHERE")) node.where = this.checkedExpr([], false);
1064
- else return node;
1084
+ else if (!this.unknownClause(INDEX_WORDS)) return node;
1065
1085
  }
1066
1086
  }
1067
1087
 
@@ -1085,7 +1105,7 @@ class PgParser extends SqlCursor {
1085
1105
  this.p++;
1086
1106
  node.with = this.reloptions();
1087
1107
  } else if (materialized && this.accept("TABLESPACE")) node.tablespace = this.nameTok("a tablespace").span;
1088
- else break;
1108
+ else if (!this.unknownClause(VIEW_WORDS)) break;
1089
1109
  }
1090
1110
  this.expect("AS");
1091
1111
  const tail = materialized
@@ -1131,7 +1151,7 @@ class PgParser extends SqlCursor {
1131
1151
  else if (this.acceptSeq("OWNED", "BY")) {
1132
1152
  if (this.accept("NONE")) node.options.push({ option: "OWNED BY", value: this.span(this.idx(-1), []) });
1133
1153
  else node.options.push({ option: "OWNED BY", value: this.qualifiedName("a column").span });
1134
- } else return node;
1154
+ } else if (!this.unknownClause(STOP)) return node;
1135
1155
  }
1136
1156
  }
1137
1157
 
@@ -1575,7 +1595,10 @@ class PgParser extends SqlCursor {
1575
1595
  const out: StatementNode[] = [];
1576
1596
  while (this.peek() !== undefined) {
1577
1597
  if (this.acceptPunct(";")) continue;
1578
- out.push(this.parse());
1598
+ const before = this.unknownClauses.length;
1599
+ const node = this.parse();
1600
+ if (this.unknownClauses.length > before) (node as { unknown?: UnknownClause[] }).unknown = this.unknownClauses.slice(before);
1601
+ out.push(node);
1579
1602
  if (this.peek() !== undefined && !this.isPunct(";")) this.fail(`unexpected token after the ${describe(out[out.length - 1]!)}`);
1580
1603
  }
1581
1604
  if (out.length === 0) this.fail("expected a statement");
@@ -197,7 +197,25 @@ function diffConstraints(key: string, before: PgDiffObject, after: PgDiffObject,
197
197
  const same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b);
198
198
  const show = (v: unknown): string | undefined => (v === undefined ? undefined : typeof v === "string" ? v : JSON.stringify(v));
199
199
 
200
+ /**
201
+ * The clauses chant does not read (#3872), compared as text: one the
202
+ * declaration has and the server does not, or has differently, is a change
203
+ * to review (SQLPG299); one only the server prints is a hint, since a
204
+ * default a newer server prints cannot be told from a clause removed.
205
+ */
206
+ function diffUnparsed(key: string, before: PgDiffObject, after: PgDiffObject, out: PgChange[], hints: string[]): void {
207
+ const b = (before.fields.unparsed as Record<string, string> | undefined) ?? {};
208
+ const a = (after.fields.unparsed as Record<string, string> | undefined) ?? {};
209
+ for (const clause of Object.keys(a)) {
210
+ if (b[clause] !== a[clause]) out.push(change(key, `unparsed.${clause}`, "SQLPG299", b[clause], a[clause], { note: "chant does not read this clause; review the change" }));
211
+ }
212
+ for (const clause of Object.keys(b)) {
213
+ if (!(clause in a)) hints.push(`${key}: the current definition has ${clause} (${b[clause]}), a clause chant does not read and the declaration does not have; it is not compared`);
214
+ }
215
+ }
216
+
200
217
  function diffObject(key: string, before: PgDiffObject, after: PgDiffObject, out: PgChange[], hints: string[], major: number): void {
218
+ diffUnparsed(key, before, after, out, hints);
201
219
  if (before.kind !== after.kind) {
202
220
  out.push(change(key, "kind", "SQLPG268", before.kind, after.kind));
203
221
  return;
@@ -797,6 +797,18 @@ export function canonicalPgObject(entityType: string, props: Record<string, unkn
797
797
  break;
798
798
  }
799
799
  }
800
+ // Clauses the parser does not know (#3872): compared as text, by clause.
801
+ const unparsed = props.unparsed as Array<{ clause: string; text: string; column?: string }> | undefined;
802
+ if (unparsed && unparsed.length > 0) {
803
+ const out: Record<string, string> = {};
804
+ for (const u of unparsed) {
805
+ const base = u.column !== undefined ? `columns.${u.column}.${u.clause}` : u.clause;
806
+ let key = base;
807
+ for (let n = 2; key in out; n++) key = `${base}#${n}`;
808
+ out[key] = canonicalExpr(u.text) ?? u.text;
809
+ }
810
+ fields.unparsed = out;
811
+ }
800
812
  return { ...base, fields: Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined && v !== false)), columns, constraints };
801
813
  }
802
814
 
@@ -350,6 +350,13 @@ export const PG_CLASSIFIER_RULES = {
350
350
  "ALTER DEFAULT PRIVILEGES changes what objects created afterwards are given; objects that exist keep their privileges.",
351
351
  `${DOCS}/sql-alterdefaultprivileges.html`,
352
352
  ),
353
+ SQLPG299: rule(
354
+ "SQLPG299",
355
+ "expand",
356
+ "Change a clause chant does not read",
357
+ "chant's parser does not know this clause, so it cannot tell which lock the change takes or whether an in-place statement makes it; the plan refuses it rather than guess. Review it: make it by hand with the ALTER the server documents for the clause, or as expand and contract.",
358
+ `${DOCS}/sql-altertable.html`,
359
+ ),
353
360
  SQLPG270: rule("SQLPG270", "drop", "Drop an object", "The object and, for a table, a materialized view or a sequence, its data are gone.", `${DOCS}/sql-droptable.html`),
354
361
  } as const satisfies Record<string, PgClassifierRule>;
355
362
 
@@ -0,0 +1,38 @@
1
+ {
2
+ "release": "REL_18_6",
3
+ "unknownClauses": [
4
+ "USING"
5
+ ],
6
+ "failures": [
7
+ "expected AS at 'USING'",
8
+ "expected IDENTITY (a generated column is GENERATED ALWAYS AS) at '('",
9
+ "expected NOT NULL, NULL or CHECK at 'REFERENCES'",
10
+ "expected NOT NULL, NULL or CHECK at 'generated'",
11
+ "expected RANGE or LIST or HASH at 'MAGIC'",
12
+ "expected a column constraint, ',' or ')' at 'NOT'",
13
+ "expected a column name ('with' is a reserved key word; quote it to use it as a name) at 'with'",
14
+ "expected a column type at ')'",
15
+ "expected a column type at ','",
16
+ "expected a partition bound value at ')'",
17
+ "expected a table name at '\\'",
18
+ "expected an expression at ')'",
19
+ "expected an index name ('ON' is a reserved key word; quote it to use it as a name) at 'ON'",
20
+ "expected the column list (CREATE TABLE ... AS is not declared here) at 'AS'",
21
+ "expected the column list (CREATE TABLE ... AS is not declared here) at 'TABLESPACE'",
22
+ "expected the column list (CREATE TABLE ... AS is not declared here) at 'USING'",
23
+ "expected the column list (CREATE TABLE ... AS is not declared here) at 'WITH'",
24
+ "expected the column list (CREATE TABLE ... AS is not declared here) at 'as'",
25
+ "expected the end of the default (a default cannot hold this operator unparenthesized) at 'IN'",
26
+ "schema elements inside CREATE SCHEMA are not declared here; declare each object on its own at 'CREATE'",
27
+ "schema elements inside CREATE SCHEMA are not declared here; declare each object on its own at 'create'",
28
+ "unexpected end of statement",
29
+ "unexpected token after the domain definition at 'ENFORCED'",
30
+ "unexpected token after the domain definition at 'NOT'",
31
+ "unexpected token after the domain definition at 'PRIMARY'",
32
+ "unexpected token after the domain definition at 'deferrable'",
33
+ "unexpected token after the domain definition at 'no'",
34
+ "unexpected token after the domain definition at 'unique'",
35
+ "unexpected token after the table definition at 'WITH'",
36
+ "unexpected token after the table definition at '\\'"
37
+ ]
38
+ }
package/src/postgres.ts CHANGED
@@ -105,3 +105,15 @@ export {
105
105
  } from "./postgres/migrate/op";
106
106
  export { POSTGRES_RECEIPTS_TABLE } from "./postgres/migrate/receipts";
107
107
  export * from "./composites/postgres";
108
+
109
+ // The build's Postgres objects as typed values, for a project's own
110
+ // `lint.policies` (#3870); also at `@intentius/chant-lexicon-sql/read`.
111
+ export {
112
+ readPostgres,
113
+ canonicalPostgresType,
114
+ type PostgresRead,
115
+ type PostgresTableInfo,
116
+ type PostgresColumnInfo,
117
+ type PostgresIndexInfo,
118
+ type PostgresViewInfo,
119
+ } from "./read/postgres";