@intentius/chant-lexicon-sql 0.124.12 → 0.124.14

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
@@ -0,0 +1,143 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { makePostSynthCtx } from "@intentius/chant-test-utils";
3
+ import type { PostSynthCheck, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
4
+ import type { SerializerResult } from "@intentius/chant/serializer";
5
+ import { sqlSerializer } from "./serializer";
6
+ import { database, table as chTable, view as chView } from "./clickhouse/entities";
7
+ import { index, schema, table, view } from "./postgres/entities";
8
+ import { canonicalPostgresType, readClickHouse, readPostgres, readSql } from "./read";
9
+ import { readPostgres as fromPostgresSubpath } from "./postgres";
10
+ import { readClickHouse as fromClickHouseSubpath } from "./clickhouse";
11
+ import { tablesOf as pgTablesOf } from "./lint/post-synth/postgres-helpers";
12
+ import { tablesOf as chTablesOf } from "./lint/post-synth/clickhouse-helpers";
13
+
14
+ const ctxOf = (entities: Record<string, unknown>) => {
15
+ const out = sqlSerializer.serialize(new Map(Object.entries(entities)) as never) as SerializerResult;
16
+ return makePostSynthCtx("sql", out.primary);
17
+ };
18
+
19
+ describe("readPostgres (#3870)", () => {
20
+ const app = schema`CREATE SCHEMA app`;
21
+ const users = table`
22
+ CREATE TABLE ${app}.users (
23
+ id bigserial PRIMARY KEY,
24
+ email text NOT NULL UNIQUE,
25
+ nickname varchar(40) DEFAULT 'anon',
26
+ tags text[],
27
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
28
+ )`;
29
+ const orders = table`
30
+ CREATE TABLE ${app}.orders (
31
+ id bigint GENERATED ALWAYS AS IDENTITY,
32
+ user_id bigint NOT NULL REFERENCES ${users} (id),
33
+ created_at timestamp,
34
+ PRIMARY KEY (id)
35
+ )`;
36
+ const ordersUser = index`CREATE INDEX orders_user_id ON ${orders} (user_id, (lower('x')))`;
37
+ const totals = view`CREATE VIEW ${app}.totals AS SELECT user_id, count(*) AS n FROM ${orders} GROUP BY user_id`;
38
+ const ctx = ctxOf({ app, users, orders, ordersUser, totals });
39
+
40
+ test("tables carry typed columns: canonical type, nullability, default, keys", () => {
41
+ const db = readPostgres(ctx);
42
+ expect(db.dialect).toBe("postgres");
43
+ expect(db.tables.map((t) => t.qualifiedName).sort()).toEqual(["app.orders", "app.users"]);
44
+ const u = db.table("users")!;
45
+ expect(u.column("id")).toMatchObject({ typeName: "bigint", nullable: false, inPrimaryKey: true });
46
+ expect(u.column("email")).toMatchObject({ typeName: "text", nullable: false, inPrimaryKey: false });
47
+ expect(u.column("nickname")).toMatchObject({ typeName: "character varying", nullable: true, default: "'anon'" });
48
+ expect(u.column("tags")).toMatchObject({ typeName: "text", array: true });
49
+ expect(u.column("created_at")).toMatchObject({ typeName: "timestamp with time zone", nullable: false, default: "now()" });
50
+ expect(u.uniques[0]!.columns).toEqual(["email"]);
51
+ expect(db.table("app.orders")!.column("id")).toMatchObject({ nullable: false, inPrimaryKey: true });
52
+ expect(db.table("orders")!.column("created_at")).toMatchObject({ typeName: "timestamp without time zone", nullable: true });
53
+ expect(db.table("orders")!.foreignKeys[0]).toMatchObject({ columns: ["user_id"], references: "users" });
54
+ });
55
+
56
+ test("indexes belong to their table and lead with their bare columns", () => {
57
+ const db = readPostgres(ctx);
58
+ expect(db.indexes).toHaveLength(1);
59
+ expect(db.indexes[0]).toMatchObject({ export: "ordersUser", table: "orders", columns: ["user_id"] });
60
+ expect(db.table("orders")!.indexes.map((i) => i.export)).toEqual(["ordersUser"]);
61
+ });
62
+
63
+ test("views name their outputs and keep their lineage", () => {
64
+ const v = readPostgres(ctx).views[0]!;
65
+ expect(v).toMatchObject({ qualifiedName: "app.totals", materialized: false, outputs: ["user_id", "n"] });
66
+ expect(v.lineage.map((e) => e.output)).toEqual(["user_id", "n"]);
67
+ });
68
+
69
+ test("canonicalPostgresType spells aliases the way typeName does", () => {
70
+ expect(canonicalPostgresType("timestamptz")).toBe("timestamp with time zone");
71
+ expect(canonicalPostgresType("INT4")).toBe("integer");
72
+ expect(canonicalPostgresType("varchar(10)")).toBe("character varying");
73
+ });
74
+
75
+ test("the checks read the same values, and the dialect subpath exports the same reader", () => {
76
+ expect(pgTablesOf(ctx)).toBe(readPostgres(ctx).tables);
77
+ expect(fromPostgresSubpath).toBe(readPostgres);
78
+ expect(readClickHouse(ctx).tables).toEqual([]);
79
+ expect(readSql(ctx)?.dialect).toBe("postgres");
80
+ });
81
+
82
+ test("reads a serialized document as well as a context", () => {
83
+ const doc = JSON.parse((sqlSerializer.serialize(new Map(Object.entries({ app, users })) as never) as SerializerResult).primary) as { dialect: string; objects: unknown[] };
84
+ expect(readPostgres(doc).table("users")!.column("email")!.nullable).toBe(false);
85
+ expect(readSql(doc as never)?.dialect).toBe("postgres");
86
+ });
87
+
88
+ test("a project policy written against the read API flags a table without created_at timestamptz NOT NULL", () => {
89
+ const TIMESTAMPTZ = canonicalPostgresType("timestamptz");
90
+ const policy: PostSynthCheck = {
91
+ id: "ORG-CREATED-AT",
92
+ description: "every table has created_at timestamptz NOT NULL",
93
+ check(c): PostSynthDiagnostic[] {
94
+ return readPostgres(c)
95
+ .tables.filter((t) => {
96
+ const col = t.column("created_at");
97
+ return !col || col.typeName !== TIMESTAMPTZ || col.nullable;
98
+ })
99
+ .map((t) => ({ checkId: "ORG-CREATED-AT", severity: "error", entity: t.export, message: `${t.qualifiedName} needs created_at timestamptz NOT NULL` }));
100
+ },
101
+ };
102
+ expect(policy.check(ctx).map((d) => d.entity)).toEqual(["orders"]);
103
+ });
104
+ });
105
+
106
+ describe("readClickHouse (#3870)", () => {
107
+ const shop = database`CREATE DATABASE shop ENGINE = Atomic`;
108
+ const events = chTable`
109
+ CREATE TABLE ${shop}.events (
110
+ ts DateTime64(3) CODEC(Delta, ZSTD),
111
+ kind LowCardinality(String),
112
+ user_id UInt64,
113
+ ref Nullable(String) COMMENT 'referrer',
114
+ label LowCardinality(Nullable(String)) DEFAULT 'x',
115
+ INDEX kind_idx kind TYPE set(100) GRANULARITY 4
116
+ )
117
+ ENGINE = ReplicatedReplacingMergeTree('/ch/{shard}/events', '{replica}', ts)
118
+ ORDER BY (kind, toDate(ts), user_id)`;
119
+ const daily = chView`CREATE VIEW ${shop}.daily AS SELECT toDate(ts) AS day, count() AS n FROM ${events} GROUP BY day`;
120
+ const ctx = ctxOf({ shop, events, daily });
121
+
122
+ test("tables carry typed columns, the engine family and the keys", () => {
123
+ const db = readClickHouse(ctx);
124
+ const t = db.table("shop.events")!;
125
+ expect(t).toMatchObject({ qualifiedName: "shop.events", engineFamily: "ReplacingMergeTree", mergeTree: true });
126
+ expect(t.sortKey).toEqual(["kind", "toDate(ts)", "user_id"]);
127
+ expect(t.primaryKeyElements).toEqual(t.sortKey);
128
+ expect(t.column("ts")).toMatchObject({ family: "DateTime64", nullable: false, inSortKey: false, codec: "Delta, ZSTD" });
129
+ expect(t.column("kind")).toMatchObject({ lowCardinality: true, baseType: "String", nullable: false, inSortKey: true });
130
+ expect(t.column("ref")).toMatchObject({ nullable: true, comment: "referrer" });
131
+ expect(t.column("label")).toMatchObject({ nullable: true, lowCardinality: true, default: { kind: "DEFAULT", expr: "'x'" } });
132
+ expect(t.indexes[0]).toMatchObject({ name: "kind_idx", type: "set(100)" });
133
+ });
134
+
135
+ test("views name their outputs; the checks and the subpath share the reader", () => {
136
+ const v = readClickHouse(ctx).views[0]!;
137
+ expect(v).toMatchObject({ materialized: false, outputs: ["day", "n"] });
138
+ expect(chTablesOf(ctx)).toBe(readClickHouse(ctx).tables);
139
+ expect(fromClickHouseSubpath).toBe(readClickHouse);
140
+ expect(readSql(ctx)?.dialect).toBe("clickhouse");
141
+ expect(readPostgres(ctx).tables).toEqual([]);
142
+ });
143
+ });
package/src/read.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `@intentius/chant-lexicon-sql/read`: a typed, read-only view of the SQL
3
+ * objects a build declares (#3870), for a project's own `lint.policies`.
4
+ *
5
+ * A policy's `check(ctx)` gets the build's parsed output in `ctx.docs`.
6
+ * {@link readPostgres} and {@link readClickHouse} turn the sql lexicon's part
7
+ * of it into tables with typed columns (SQL type, nullability, default,
8
+ * comment), keys, indexes and engines, and views with their outputs and
9
+ * column lineage. The lexicon's own post-synth checks read the same values,
10
+ * so a project rule sees exactly what `SQLPG101` or `SQLCH113` sees.
11
+ *
12
+ * ```ts
13
+ * import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
14
+ * import { canonicalPostgresType, readPostgres } from "@intentius/chant-lexicon-sql/read";
15
+ *
16
+ * const TIMESTAMPTZ = canonicalPostgresType("timestamptz");
17
+ *
18
+ * export const createdAt: PostSynthCheck = {
19
+ * id: "ORG-CREATED-AT",
20
+ * description: "every table has created_at timestamptz NOT NULL",
21
+ * check(ctx) {
22
+ * return readPostgres(ctx).tables
23
+ * .filter((t) => { const c = t.column("created_at"); return !c || c.typeName !== TIMESTAMPTZ || c.nullable; })
24
+ * .map((t) => ({ checkId: "ORG-CREATED-AT", severity: "error", entity: t.export,
25
+ * message: `${t.qualifiedName} needs created_at timestamptz NOT NULL` }));
26
+ * },
27
+ * };
28
+ * ```
29
+ *
30
+ * Every value keeps the fields the serializer writes (see the Serialization
31
+ * page) and adds the derived ones documented on each type. The reads also take
32
+ * the serialized document itself (`{ dialect, objects }`), for a tool that
33
+ * holds a recorded build rather than a post-synth context.
34
+ */
35
+
36
+ export { readPostgres, canonicalPostgresType } from "./read/postgres";
37
+ export type { PostgresRead, PostgresTableInfo, PostgresColumnInfo, PostgresIndexInfo, PostgresViewInfo } from "./read/postgres";
38
+ export { readClickHouse } from "./read/clickhouse";
39
+ export type { ClickHouseRead, ClickHouseTableInfo, ClickHouseColumnInfo, ClickHouseViewInfo } from "./read/clickhouse";
40
+ export { sqlDocuments, isSqlOutputDocument } from "./read/objects";
41
+ export type {
42
+ SqlReadSource,
43
+ SqlOutputDocument,
44
+ SqlObjectBase,
45
+ SqlObjectJson,
46
+ RefJson,
47
+ LineageJson,
48
+ PostgresTableJson,
49
+ PostgresIndexJson,
50
+ PostgresViewJson,
51
+ PostgresForeignKeyJson,
52
+ ClickHouseTableJson,
53
+ ClickHouseViewJson,
54
+ } from "./read/objects";
55
+
56
+ import { readClickHouse, type ClickHouseRead } from "./read/clickhouse";
57
+ import { readPostgres, type PostgresRead } from "./read/postgres";
58
+ import { sqlDocuments, type SqlOutputDocument, type SqlReadSource } from "./read/objects";
59
+
60
+ /**
61
+ * The build's SQL objects in whichever dialect it declares: one build holds
62
+ * one dialect. Undefined when the build has no sql output.
63
+ */
64
+ export function readSql(source: SqlReadSource | SqlOutputDocument): PostgresRead | ClickHouseRead | undefined {
65
+ const dialect = "objects" in source ? source.dialect : sqlDocuments(source)[0]?.dialect;
66
+ if (dialect === "postgres") return readPostgres(source);
67
+ if (dialect === "clickhouse") return readClickHouse(source);
68
+ return undefined;
69
+ }
package/src/serializer.ts CHANGED
@@ -25,6 +25,9 @@
25
25
  * targets (`sql.postgresMajor`, else the newest pinned major). Post-synth
26
26
  * checks and the lock classifier read it; ClickHouse output has no such field.
27
27
  *
28
+ * A clause chant's parser does not know (#3872) is kept in the statement as
29
+ * written and named in a build warning ({@link unparsedWarnings}).
30
+ *
28
31
  * Rule ids: `SQL` for rules that hold in every dialect, `SQLCH` for the
29
32
  * ClickHouse dialect's (#3199), `SQLPG` for Postgres's (#3289).
30
33
  */
@@ -36,10 +39,39 @@ import { isClickHouseObject, type ClickHouseObject } from "./clickhouse/entities
36
39
  import { isPostgresObject, type PostgresObject } from "./postgres/entities";
37
40
  import { POSTGRES_LATEST_MAJOR } from "./spec/postgres-pin";
38
41
  import { applyOrder, lineageJson, referenceName, type LineageEdge } from "./core/references";
39
- import { sqlFileReferences } from "./sql-files";
42
+ import { sqlFileLineOf, sqlFileOf, sqlFileReferences } from "./sql-files";
43
+ import { relative } from "node:path";
44
+ import { getProvenance } from "@intentius/chant/provenance";
45
+ import type { UnparsedClause } from "./core/interpolation";
40
46
 
41
47
  export { applyOrder } from "./core/references";
42
48
 
49
+ /**
50
+ * A warning for each clause chant's parser does not know (#3872): where it
51
+ * is (the `.sql` file and line, or the template's file and the line in its
52
+ * statement), which object, and what follows from it. The statement is sent
53
+ * as written; the server decides.
54
+ */
55
+ export function unparsedWarnings(objects: ReadonlyMap<string, Declarable>): string[] {
56
+ const out: string[] = [];
57
+ for (const [name, entity] of objects) {
58
+ const unparsed = (entity as unknown as { props?: { unparsed?: UnparsedClause[] } }).props?.unparsed;
59
+ if (!unparsed || unparsed.length === 0) continue;
60
+ const file = sqlFileOf(entity);
61
+ const start = sqlFileLineOf(entity);
62
+ const source = getProvenance(entity)?.sourceFile;
63
+ for (const u of unparsed) {
64
+ const where = file !== undefined ? `${file}:${start !== undefined ? start + u.line - 1 : u.line}` : `${source ? relative(process.cwd(), source) : "?"}: ${name}, line ${u.line} of its statement`;
65
+ const on = u.column !== undefined ? ` on column ${u.column}` : "";
66
+ out.push(
67
+ `${where}: ${(entity as { sqlName?: string }).sqlName ?? name} has a clause chant does not read, ${u.clause}${on}. ` +
68
+ "It is sent as written and the server checks it; a plan reads the rest of the object, and classes any change to the clause as one to review",
69
+ );
70
+ }
71
+ }
72
+ return out;
73
+ }
74
+
43
75
  /** The file the statements are written to, beside the primary output. */
44
76
  export const CLICKHOUSE_DDL_FILE = "clickhouse.sql";
45
77
 
@@ -104,10 +136,12 @@ function serializePostgres(
104
136
  objects: order.map((n) => postgresObjectJson(n, objects.get(n)!, names, extra.get(n))),
105
137
  };
106
138
  const ddl = order.map((n) => `${(objects.get(n)!.props as { ddl: string }).ddl};`).join("\n\n");
139
+ const warnings = unparsedWarnings(objects);
107
140
  return {
108
141
  primary: `${JSON.stringify(doc, null, 2)}\n`,
109
142
  files: { [POSTGRES_DDL_FILE]: `${ddl}\n` },
110
143
  verbatimFiles: [POSTGRES_DDL_FILE],
144
+ ...(warnings.length > 0 ? { warnings } : {}),
111
145
  };
112
146
  }
113
147
 
@@ -144,10 +178,12 @@ export const sqlSerializer: Serializer = {
144
178
  const ddl = order
145
179
  .map((n) => `${(objects.get(n)!.props as { ddl: string }).ddl};`)
146
180
  .join("\n\n");
181
+ const warnings = unparsedWarnings(objects);
147
182
  return {
148
183
  primary: `${JSON.stringify(doc, null, 2)}\n`,
149
184
  files: { [CLICKHOUSE_DDL_FILE]: `${ddl}\n` },
150
185
  verbatimFiles: [CLICKHOUSE_DDL_FILE],
186
+ ...(warnings.length > 0 ? { warnings } : {}),
151
187
  };
152
188
  },
153
189
  };
package/src/sql-files.ts CHANGED
@@ -18,7 +18,9 @@
18
18
  * `MATERIALIZED VIEW`, `DICTIONARY`, `FUNCTION`, `USER`, `ROLE` and `ROW
19
19
  * POLICY`, and `GRANT` (#3711). Any other statement, and any
20
20
  * statement that does not parse, throws a {@link SqlFileError} naming every
21
- * such statement: none is left out.
21
+ * such statement: none is left out. A statement that parses except for a
22
+ * clause the parser does not know (#3872) is read, kept as written, and the
23
+ * clause listed in its object's `unparsed` with the line it is on.
22
24
  *
23
25
  * A name is a reference where it is qualified (`app.users`), or bare where an
24
26
  * object is named (`REFERENCES users`, `FROM users`, an index's table, a
@@ -27,7 +29,7 @@
27
29
 
28
30
  import type { Declarable } from "@intentius/chant/declarable";
29
31
  import type { SqlDialect } from "./dialects";
30
- import { SqlFileError, type ReadOptions } from "./files/common";
32
+ import { SqlFileError, type FileLine, type ReadOptions } from "./files/common";
31
33
  import { postgresTag, readPostgres, templateStrings } from "./files/postgres";
32
34
  import { clickhouseTag, readClickHouse } from "./files/clickhouse";
33
35
  import { exportNames as pgExportNames, objectsToIR as pgObjectsToIR, type ImportedPgObject } from "./postgres/import/ir";
@@ -52,6 +54,14 @@ export interface SqlFileObject {
52
54
  ddl: string;
53
55
  /** The file it came from, the `origin` it was read under. */
54
56
  origin: string;
57
+ /** The line its statement starts on in the file, from 1. */
58
+ line?: number;
59
+ /**
60
+ * Clauses in it chant's parser does not know (#3872), each with the line
61
+ * it starts on: kept in `ddl` as written, for the server to check. Absent
62
+ * when there are none.
63
+ */
64
+ unparsed?: Array<{ clause: string; line: number; column?: string }>;
55
65
  }
56
66
 
57
67
  /** A file to read: its DDL and the name errors give it. */
@@ -64,8 +74,8 @@ type Imported = ImportedPgObject | ImportedObject;
64
74
  const qualifierOf = (o: Imported): string | undefined => ("schema" in o ? o.schema : "database" in o ? o.database : undefined);
65
75
  const qualifiedName = (o: Imported): string => (qualifierOf(o) ? `${qualifierOf(o)}.${o.name}` : o.name);
66
76
 
67
- function readObjects(dialect: SqlDialect, sources: readonly SqlFileSource[]): Array<Imported & { origin: string }> {
68
- const out: Array<Imported & { origin: string }> = [];
77
+ function readObjects(dialect: SqlDialect, sources: readonly SqlFileSource[]): Array<Imported & FileLine & { origin: string }> {
78
+ const out: Array<Imported & FileLine & { origin: string }> = [];
69
79
  for (const source of sources) {
70
80
  const problems: string[] = [];
71
81
  const objects = dialect === "postgres" ? readPostgres(source.ddl, source, problems) : readClickHouse(source.ddl, source, problems);
@@ -75,6 +85,9 @@ function readObjects(dialect: SqlDialect, sources: readonly SqlFileSource[]): Ar
75
85
  return out;
76
86
  }
77
87
 
88
+ /** An object's line and unknown clauses, the fields that are set. */
89
+ const fileMeta = (o: FileLine): FileLine => ({ ...(o.line !== undefined ? { line: o.line } : {}), ...(o.unparsed ? { unparsed: o.unparsed } : {}) });
90
+
78
91
  /** Export names for objects read from files, unique across them. */
79
92
  function namesFor(dialect: SqlDialect, objects: readonly Imported[]): string[] {
80
93
  return dialect === "postgres" ? pgExportNames(objects as ImportedPgObject[]) : chExportNames(objects as ImportedObject[]);
@@ -84,7 +97,7 @@ function namesFor(dialect: SqlDialect, objects: readonly Imported[]): string[] {
84
97
  export function readSqlFile(dialect: SqlDialect, ddl: string, options: ReadOptions): SqlFileObject[] {
85
98
  const objects = readObjects(dialect, [{ ...options, ddl }]);
86
99
  const names = namesFor(dialect, objects);
87
- return objects.map((o, i) => ({ export: names[i]!, type: o.type, name: qualifiedName(o), ddl: o.ddl, origin: o.origin }));
100
+ return objects.map((o, i) => ({ export: names[i]!, type: o.type, name: qualifiedName(o), ddl: o.ddl, origin: o.origin, ...fileMeta(o) }));
88
101
  }
89
102
 
90
103
  /**
@@ -100,7 +113,7 @@ export function sqlFileDeclarations(dialect: SqlDialect, ddl: string, options: R
100
113
  if (!file) throw new SqlFileError(options.origin, ["the DDL creates no objects"]);
101
114
  return {
102
115
  content: file.content,
103
- objects: ir.resources.map((r, i) => ({ export: r.logicalId, type: r.type, name: qualifiedName(objects[i]!), ddl: objects[i]!.ddl, origin: options.origin })),
116
+ objects: ir.resources.map((r, i) => ({ export: r.logicalId, type: r.type, name: qualifiedName(objects[i]!), ddl: objects[i]!.ddl, origin: options.origin, ...fileMeta(objects[i]!) })),
104
117
  };
105
118
  }
106
119
 
@@ -109,7 +122,13 @@ export function sqlFileOf(entity: unknown): string | undefined {
109
122
  return typeof entity === "object" && entity !== null ? ((entity as Record<symbol, unknown>)[FROM_FILE] as string | undefined) : undefined;
110
123
  }
111
124
 
125
+ /** The line an entity read from a `.sql` file starts on in it. Undefined for any other entity. */
126
+ export function sqlFileLineOf(entity: unknown): number | undefined {
127
+ return typeof entity === "object" && entity !== null ? ((entity as Record<symbol, unknown>)[FROM_LINE] as number | undefined) : undefined;
128
+ }
129
+
112
130
  const FROM_FILE = Symbol.for("chant.sql.fromFile");
131
+ const FROM_LINE = Symbol.for("chant.sql.fromLine");
113
132
 
114
133
  /** A declared entity as a template target: its export, type and name. */
115
134
  function targetOf(dialect: SqlDialect, exportName: string, entity: Declarable): PgTemplateItem | undefined {
@@ -150,6 +169,7 @@ export function sqlFileEntities(dialect: SqlDialect, sources: readonly SqlFileSo
150
169
 
151
170
  const items: PgTemplateItem[] = objects.map((o, i) => ({ exportName: names[i]!, type: o.type, ...("schema" in o && o.schema ? { schema: o.schema } : {}), ...("database" in o && o.database ? { database: o.database } : {}), name: o.name, ddl: o.ddl }) as PgTemplateItem);
152
171
  const origin = new Map(objects.map((o, i) => [names[i]!, o.origin]));
172
+ const lineOf = new Map(objects.map((o, i) => [names[i]!, o.line]));
153
173
  const knownItems = [...known].map(([n, e]) => targetOf(dialect, n, e)).filter((t): t is PgTemplateItem => t !== undefined);
154
174
  const declared = new Map(knownItems.map((t) => [`${t.type} ${qualifiedName(t as Imported)}`, t.exportName]));
155
175
  const seen = new Map<string, string>();
@@ -186,6 +206,7 @@ export function sqlFileEntities(dialect: SqlDialect, sources: readonly SqlFileSo
186
206
  try {
187
207
  const entity = tagOf(it.type)!(templateStrings(body.parts), ...values);
188
208
  Object.defineProperty(entity, FROM_FILE, { value: origin.get(it.exportName), enumerable: false });
209
+ if (lineOf.get(it.exportName) !== undefined) Object.defineProperty(entity, FROM_LINE, { value: lineOf.get(it.exportName), enumerable: false });
189
210
  built.set(it.exportName, entity);
190
211
  } catch (e) {
191
212
  failed.add(it.exportName);
package/src/sql-types.ts CHANGED
@@ -22,6 +22,9 @@
22
22
  * (`./lint/post-synth/postgres-names.ts`'s `typeIdentity`), the type
23
23
  * family in ClickHouse; `never` when it cannot be told.
24
24
  *
25
+ * A column that carries a clause chant does not read (#3872) keeps the type
26
+ * that was read, with `nullable` and `cmp` `never`.
27
+ *
25
28
  * A view's column that is one column reference (`${t.columns.c}`, `o.${t.columns.c}`)
26
29
  * is that column's type; any other expression is `unknown`.
27
30
  */
@@ -108,6 +111,17 @@ interface RelationEntry {
108
111
 
109
112
  const UNKNOWN_COLUMN = (name: string): ColumnEntry => ({ name, sqlType: "unknown", ts: "unknown", nullable: "never" });
110
113
 
114
+ /** The columns that carry a clause chant does not read (#3872). */
115
+ const opaqueColumns = (o: OutputObject): Set<string> =>
116
+ new Set(((o.unparsed as Array<{ column?: string }> | undefined) ?? []).flatMap((u) => (u.column !== undefined ? [u.column] : [])));
117
+
118
+ /**
119
+ * A column with a clause chant does not read (#3872): its type is what was
120
+ * read, but whether it can be NULL and how it compares in a key are not
121
+ * told, so it fits every slot rather than a guessed one.
122
+ */
123
+ const opaque = (c: ColumnEntry): ColumnEntry => ({ name: c.name, sqlType: c.sqlType, ts: c.ts, nullable: "never" });
124
+
111
125
  const posix = (p: string) => p.split(sep).join("/");
112
126
 
113
127
  /** Where a declaration came from: its file relative to the project, and its export. */
@@ -215,13 +229,15 @@ function postgresEntries(objects: OutputObject[], major: number, entities: Reado
215
229
  for (const c of pt?.columns ?? []) if (!inherited.has(c.name)) inherited.set(c.name, c.type);
216
230
  }
217
231
  const pk = new Set(t.primaryKey?.columns ?? []);
232
+ const unread = opaqueColumns(o);
218
233
  columns = names.map((name) => {
219
234
  const c = own.get(name);
220
235
  const text = c?.type ?? inherited.get(name);
221
236
  if (text === undefined) return UNKNOWN_COLUMN(name);
222
237
  const ty = pgType(p, text);
223
238
  const notNull = c?.notNull === true || pk.has(name) || c?.generated?.kind === "identity" || ty.notNull;
224
- return { name, sqlType: ty.sqlType, ts: ty.ts, nullable: !notNull, cmp: typeIdentity(p, text) };
239
+ const entry: ColumnEntry = { name, sqlType: ty.sqlType, ts: ty.ts, nullable: !notNull, cmp: typeIdentity(p, text) };
240
+ return unread.has(name) ? opaque(entry) : entry;
225
241
  });
226
242
  } else {
227
243
  columns = names.map((name) => {
@@ -265,9 +281,10 @@ function clickhouseEntries(objects: OutputObject[], entities: ReadonlyMap<string
265
281
  const entity = entities.get(exportName);
266
282
  if (!o || !entity || !CH_RELATIONS.has(o.type)) return undefined;
267
283
  const declared = new Map(((o.columns as Array<{ name: string; type?: string; nullable?: boolean }> | undefined) ?? []).map((c) => [c.name, c]));
284
+ const unread = opaqueColumns(o);
268
285
  const columns = runtimeColumns(entity).map((name) => {
269
286
  const c = declared.get(name);
270
- if (c) return chColumn(name, c.type, c.nullable);
287
+ if (c) return unread.has(name) ? opaque(chColumn(name, c.type, c.nullable)) : chColumn(name, c.type, c.nullable);
271
288
  const via = passthrough(o, name, quoteIdentifier);
272
289
  const source = via && entryOf(via.export)?.columns.find((x) => x.name === via.column);
273
290
  return source ? { ...source, name } : UNKNOWN_COLUMN(name);