@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.
- package/dist/clickhouse/apply/statements.d.ts.map +1 -1
- package/dist/clickhouse/entities.d.ts +7 -1
- package/dist/clickhouse/entities.d.ts.map +1 -1
- package/dist/clickhouse/expressions.d.ts +27 -0
- package/dist/clickhouse/expressions.d.ts.map +1 -0
- package/dist/clickhouse/parser.d.ts +12 -2
- package/dist/clickhouse/parser.d.ts.map +1 -1
- package/dist/clickhouse/plan/diff.d.ts.map +1 -1
- package/dist/clickhouse/plan/normalize.d.ts +11 -1
- package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
- package/dist/clickhouse/plan/rebuild-handoff.d.ts.map +1 -1
- package/dist/clickhouse/plan/rules.d.ts +1 -0
- package/dist/clickhouse/plan/rules.d.ts.map +1 -1
- package/dist/clickhouse.d.ts +1 -0
- package/dist/clickhouse.d.ts.map +1 -1
- package/dist/codegen/classifier-page.d.ts.map +1 -1
- package/dist/codegen/postgres-classifier-page.d.ts.map +1 -1
- package/dist/core/cursor.d.ts +41 -0
- package/dist/core/cursor.d.ts.map +1 -1
- package/dist/core/interpolation.d.ts +18 -1
- package/dist/core/interpolation.d.ts.map +1 -1
- package/dist/files/clickhouse.d.ts +2 -2
- package/dist/files/clickhouse.d.ts.map +1 -1
- package/dist/files/common.d.ts +16 -0
- package/dist/files/common.d.ts.map +1 -1
- package/dist/files/postgres.d.ts +2 -2
- package/dist/files/postgres.d.ts.map +1 -1
- package/dist/import-parser.d.ts +2 -1
- package/dist/import-parser.d.ts.map +1 -1
- package/dist/integrity.json +19 -19
- package/dist/lint/post-synth/clickhouse-helpers.d.ts +6 -19
- package/dist/lint/post-synth/clickhouse-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/postgres-helpers.d.ts +15 -55
- package/dist/lint/post-synth/postgres-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/postgres-names.d.ts +2 -2
- package/dist/lint/post-synth/postgres-names.d.ts.map +1 -1
- package/dist/lint/post-synth/sql-helpers.d.ts +5 -6
- package/dist/lint/post-synth/sql-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch102.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch103.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch104.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch105.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch106.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch107.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch111.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch112.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch113.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch114.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch115.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch120.d.ts.map +1 -1
- package/dist/lint/post-synth/sqlch125.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/postgres/apply/statements.d.ts.map +1 -1
- package/dist/postgres/entities.d.ts +7 -1
- package/dist/postgres/entities.d.ts.map +1 -1
- package/dist/postgres/parser.d.ts +10 -2
- package/dist/postgres/parser.d.ts.map +1 -1
- package/dist/postgres/plan/diff.d.ts.map +1 -1
- package/dist/postgres/plan/normalize.d.ts.map +1 -1
- package/dist/postgres/plan/rules.d.ts +1 -0
- package/dist/postgres/plan/rules.d.ts.map +1 -1
- package/dist/postgres.d.ts +1 -0
- package/dist/postgres.d.ts.map +1 -1
- package/dist/read/clickhouse.d.ts +66 -0
- package/dist/read/clickhouse.d.ts.map +1 -0
- package/dist/read/objects.d.ts +99 -0
- package/dist/read/objects.d.ts.map +1 -0
- package/dist/read/postgres.d.ts +83 -0
- package/dist/read/postgres.d.ts.map +1 -0
- package/dist/read.d.ts +49 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/rules/clickhouse-helpers.ts +20 -97
- package/dist/rules/postgres-helpers.ts +18 -53
- package/dist/rules/postgres-names.ts +6 -6
- package/dist/rules/sql-helpers.ts +8 -28
- package/dist/rules/sqlch102.ts +2 -3
- package/dist/rules/sqlch103.ts +2 -3
- package/dist/rules/sqlch104.ts +2 -3
- package/dist/rules/sqlch105.ts +2 -3
- package/dist/rules/sqlch106.ts +2 -3
- package/dist/rules/sqlch107.ts +2 -3
- package/dist/rules/sqlch111.ts +2 -3
- package/dist/rules/sqlch112.ts +2 -3
- package/dist/rules/sqlch113.ts +2 -3
- package/dist/rules/sqlch114.ts +2 -3
- package/dist/rules/sqlch115.ts +2 -3
- package/dist/rules/sqlch120.ts +2 -3
- package/dist/rules/sqlch125.ts +2 -3
- package/dist/serializer.d.ts +11 -1
- package/dist/serializer.d.ts.map +1 -1
- package/dist/sql-files.d.ts +17 -1
- package/dist/sql-files.d.ts.map +1 -1
- package/dist/sql-types.d.ts +3 -0
- package/dist/sql-types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/clickhouse/apply/statements.ts +2 -0
- package/src/clickhouse/entities.ts +8 -1
- package/src/clickhouse/expressions.ts +108 -0
- package/src/clickhouse/parser.ts +113 -31
- package/src/clickhouse/plan/diff.ts +19 -0
- package/src/clickhouse/plan/normalize.ts +27 -2
- package/src/clickhouse/plan/rebuild-handoff.ts +2 -1
- package/src/clickhouse/plan/rules.ts +7 -0
- package/src/clickhouse/testdata/server-ddl-baseline.json +67 -0
- package/src/clickhouse.ts +10 -0
- package/src/codegen/classifier-page.ts +2 -0
- package/src/codegen/postgres-classifier-page.ts +2 -0
- package/src/core/cursor.ts +96 -0
- package/src/core/interpolation.ts +32 -1
- package/src/files/clickhouse.ts +7 -5
- package/src/files/common.ts +23 -0
- package/src/files/postgres.ts +12 -8
- package/src/import-parser.ts +2 -1
- package/src/lint/post-synth/clickhouse-helpers.ts +20 -97
- package/src/lint/post-synth/postgres-helpers.ts +18 -53
- package/src/lint/post-synth/postgres-names.ts +6 -6
- package/src/lint/post-synth/sql-helpers.ts +8 -28
- package/src/lint/post-synth/sqlch102.ts +2 -3
- package/src/lint/post-synth/sqlch103.ts +2 -3
- package/src/lint/post-synth/sqlch104.ts +2 -3
- package/src/lint/post-synth/sqlch105.ts +2 -3
- package/src/lint/post-synth/sqlch106.ts +2 -3
- package/src/lint/post-synth/sqlch107.ts +2 -3
- package/src/lint/post-synth/sqlch111.ts +2 -3
- package/src/lint/post-synth/sqlch112.ts +2 -3
- package/src/lint/post-synth/sqlch113.ts +2 -3
- package/src/lint/post-synth/sqlch114.ts +2 -3
- package/src/lint/post-synth/sqlch115.ts +2 -3
- package/src/lint/post-synth/sqlch120.ts +2 -3
- package/src/lint/post-synth/sqlch125.ts +2 -3
- package/src/postgres/apply/statements.ts +2 -0
- package/src/postgres/entities.ts +9 -1
- package/src/postgres/parser.ts +31 -8
- package/src/postgres/plan/diff.ts +18 -0
- package/src/postgres/plan/normalize.ts +12 -0
- package/src/postgres/plan/rules.ts +7 -0
- package/src/postgres/testdata/server-ddl-baseline.json +38 -0
- package/src/postgres.ts +12 -0
- package/src/read/clickhouse.ts +137 -0
- package/src/read/objects.ts +137 -0
- package/src/read/postgres.ts +180 -0
- package/src/read-policies.e2e.test.ts +144 -0
- package/src/read.test.ts +143 -0
- package/src/read.ts +69 -0
- package/src/serializer.ts +37 -1
- package/src/sql-files.ts +27 -6
- package/src/sql-types.ts +19 -2
- package/src/unknown-clauses.test.ts +227 -0
package/src/read.test.ts
ADDED
|
@@ -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
|
-
|
|
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);
|