@intentius/chant-lexicon-sql 0.124.22 → 0.124.23
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/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/workspace-outputs.d.ts +50 -0
- package/dist/workspace-outputs.d.ts.map +1 -0
- package/package.json +5 -3
- package/src/plugin.ts +11 -0
- package/src/workspace-kinds.test.ts +21 -0
- package/src/workspace-outputs.e2e.test.ts +222 -0
- package/src/workspace-outputs.test.ts +82 -0
- package/src/workspace-outputs.ts +199 -0
- package/workspace-kinds.json +30 -0
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "6ff9ade6841fa9bd2bc93e37a99a7f62be197241e6f265497ec9835e71d4a3cb",
|
|
5
5
|
"meta.json": "59ca03876cce0d293e15a7f0a90ef7f250d133e46773c73fedede4a2ec792119",
|
|
6
6
|
"types/index.d.ts": "0286ebb367773e84c76818b0f69cb14cff8328fd2f8c65ed87563c8d5815458c",
|
|
7
7
|
"rules/postgres-templates.ts": "082da542239ab9f9627026972281102a61ca269cb79e2514f402dfc400ccdcde",
|
|
@@ -79,5 +79,5 @@
|
|
|
79
79
|
"rules/sqlpg125.ts": "cc7411c57c525613dc720ba00e986bbc4e5a84b8b0bcfa7a6ce0d058f039f025",
|
|
80
80
|
"rules/sqlpg126.ts": "d57b82a5842273876510131d67bf4418cbd61eff821e245edebc644dafd502bb"
|
|
81
81
|
},
|
|
82
|
-
"composite": "
|
|
82
|
+
"composite": "74fc50b802b5f7130fc4443a7b892d34776926c9da75827355ceec10529b4ba0"
|
|
83
83
|
}
|
package/dist/manifest.json
CHANGED
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAgB,aAAa,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAgB,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAyB5E,eAAO,MAAM,SAAS,EAAE,aA+PvB,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workspace outputs a sql member exposes (#3931): each table and view,
|
|
3
|
+
* and each of their columns, as names another member's link may target.
|
|
4
|
+
*
|
|
5
|
+
* | Dialect | Relation | Column |
|
|
6
|
+
* |---|---|---|
|
|
7
|
+
* | Postgres | `<schema>.<table>` | `<schema>.<table>.<column>` |
|
|
8
|
+
* | ClickHouse | `<database>.<table>` | `<database>.<table>.<column>` |
|
|
9
|
+
*
|
|
10
|
+
* The schema is `public` when the DDL does not qualify the name, and the
|
|
11
|
+
* database `default`. Views and materialized views are relations too.
|
|
12
|
+
*
|
|
13
|
+
* Names are the declared ones, read from the entities the member's source
|
|
14
|
+
* folds to, never an environment's or a profile's rendering: a member that
|
|
15
|
+
* renames its database per environment or tenant at deploy time exposes the
|
|
16
|
+
* same outputs everywhere.
|
|
17
|
+
*
|
|
18
|
+
* Identifiers are written unquoted, in the case the dialect keeps:
|
|
19
|
+
*
|
|
20
|
+
* - Postgres folds an unquoted identifier to lower case and keeps a quoted
|
|
21
|
+
* one exactly, so `CREATE TABLE Orders` is `public.orders` and `CREATE
|
|
22
|
+
* TABLE "Orders"` is `public.Orders`, a different output;
|
|
23
|
+
* - ClickHouse is case-sensitive, so a name is kept as written, quoted or not.
|
|
24
|
+
*
|
|
25
|
+
* A link's near-name hint compares names case-insensitively, so a link to
|
|
26
|
+
* `public.orders` when only `public.Orders` exists is pointed at it.
|
|
27
|
+
*
|
|
28
|
+
* A view's columns are its declared column list, or else the names of its
|
|
29
|
+
* select list (an alias, or the column a bare reference reads). A select
|
|
30
|
+
* list item with no name chant can derive, or a `*`, leaves the view's
|
|
31
|
+
* columns incomplete: that is a gap, so a link to one of them is unresolved
|
|
32
|
+
* rather than missing.
|
|
33
|
+
*
|
|
34
|
+
* A `-- previously: <old>` rename hint, on a column's line or before the
|
|
35
|
+
* CREATE, is reported as the output's previous name, so a link to the old
|
|
36
|
+
* name says where it went.
|
|
37
|
+
*/
|
|
38
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
39
|
+
import type { LexiconUnreadSource, LexiconWorkspaceOutputs } from "@intentius/chant/graph-ir";
|
|
40
|
+
/** The schema an unqualified Postgres name is created in. */
|
|
41
|
+
export declare const POSTGRES_DEFAULT_SCHEMA = "public";
|
|
42
|
+
/** The database an unqualified ClickHouse name is created in. */
|
|
43
|
+
export declare const CLICKHOUSE_DEFAULT_DATABASE = "default";
|
|
44
|
+
/**
|
|
45
|
+
* The `workspaceOutputs` hook: every table and view the entities declare,
|
|
46
|
+
* and their columns. `unread` names the member's files that did not fold;
|
|
47
|
+
* one that imports the sql lexicon may declare tables, so it is a gap.
|
|
48
|
+
*/
|
|
49
|
+
export declare function sqlWorkspaceOutputs(entities: ReadonlyMap<string, Declarable>, unread?: readonly LexiconUnreadSource[]): LexiconWorkspaceOutputs | undefined;
|
|
50
|
+
//# sourceMappingURL=workspace-outputs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-outputs.d.ts","sourceRoot":"","sources":["../src/workspace-outputs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAE,mBAAmB,EAA0B,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAOtH,6DAA6D;AAC7D,eAAO,MAAM,uBAAuB,WAAW,CAAC;AAChD,iEAAiE;AACjE,eAAO,MAAM,2BAA2B,YAAY,CAAC;AA4HrD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAE,SAAS,mBAAmB,EAAO,GAAG,uBAAuB,GAAG,SAAS,CAoB/J"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-sql",
|
|
3
|
-
"version": "0.124.
|
|
3
|
+
"version": "0.124.23",
|
|
4
4
|
"description": "SQL lexicon for chant — database schema as typed declarations, one dialect at a time; ClickHouse first, with types read from a pinned server's catalog",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"type": "module",
|
|
25
25
|
"files": [
|
|
26
26
|
"src/",
|
|
27
|
-
"dist/"
|
|
27
|
+
"dist/",
|
|
28
|
+
"workspace-kinds.json"
|
|
28
29
|
],
|
|
29
30
|
"exports": {
|
|
30
31
|
".": {
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
"types": "./dist/index.d.ts",
|
|
33
34
|
"default": "./src/index.ts"
|
|
34
35
|
},
|
|
36
|
+
"./workspace-kinds": "./workspace-kinds.json",
|
|
35
37
|
"./*": {
|
|
36
38
|
"development": "./src/*.ts",
|
|
37
39
|
"types": "./dist/*.d.ts",
|
|
@@ -70,6 +72,6 @@
|
|
|
70
72
|
"typescript": "^5.9.3"
|
|
71
73
|
},
|
|
72
74
|
"peerDependencies": {
|
|
73
|
-
"@intentius/chant": "^0.124.
|
|
75
|
+
"@intentius/chant": "^0.124.23"
|
|
74
76
|
}
|
|
75
77
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { CLICKHOUSE_EMULATOR } from "./op/activities/clickhouse-emulator";
|
|
|
29
29
|
import { POSTGRES_EMULATOR } from "./op/activities/postgres-emulator";
|
|
30
30
|
import { sqlSkills } from "./skill-defs";
|
|
31
31
|
import { detectTemplate } from "./detect";
|
|
32
|
+
import { sqlWorkspaceOutputs } from "./workspace-outputs";
|
|
32
33
|
import { CDC_TEMPLATE, DEFAULT_TEMPLATE, EVENTS_TEMPLATE, POSTGRES_EVENTS_TEMPLATE, POSTGRES_TEMPLATE, POSTGRES_TENANT_TEMPLATE } from "./init-templates";
|
|
33
34
|
import { compositeCatalog } from "./composites/catalog";
|
|
34
35
|
|
|
@@ -192,6 +193,16 @@ export const sqlPlugin: LexiconPlugin = {
|
|
|
192
193
|
return sqlFilesBuildRoot(ctx);
|
|
193
194
|
},
|
|
194
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Each table and view, and each of their columns, as workspace outputs
|
|
198
|
+
* (`./workspace-outputs.ts`, #3931): `public.orders`,
|
|
199
|
+
* `public.orders.customer_id`. Another member's link to a dropped or
|
|
200
|
+
* renamed column then fails `chant workspace check`.
|
|
201
|
+
*/
|
|
202
|
+
workspaceOutputs(entities, unread) {
|
|
203
|
+
return sqlWorkspaceOutputs(entities, unread);
|
|
204
|
+
},
|
|
205
|
+
|
|
195
206
|
/** One generator for both dialects: each IR resource's type says whose declarations it is. */
|
|
196
207
|
templateGenerator() {
|
|
197
208
|
return sqlTemplateGenerator;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dbt member kind this lexicon supplies to a chant workspace (#3932). It
|
|
3
|
+
* is data in `workspace-kinds.json` at the package root, exported at
|
|
4
|
+
* `./workspace-kinds`; chant reads it without importing the lexicon. A dbt
|
|
5
|
+
* project's sources read the tables a sql member declares.
|
|
6
|
+
*/
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { describeWorkspaceKindConformance } from "@intentius/chant-test-utils";
|
|
9
|
+
|
|
10
|
+
const project = "name: analytics\nversion: '1.0'\nprofile: analytics\n";
|
|
11
|
+
|
|
12
|
+
describeWorkspaceKindConformance({
|
|
13
|
+
packageDir: join(import.meta.dirname, ".."),
|
|
14
|
+
scenarios: [
|
|
15
|
+
{ name: "a dbt project", kind: "dbt", files: { "dbt_project.yml": project, "models/sources.yml": "version: 2\n" }, claims: true },
|
|
16
|
+
// chant's own precedence (500) outranks dbt's (300): no tie.
|
|
17
|
+
{ name: "a chant project beside a dbt_project.yml", kind: "dbt", files: { "dbt_project.yml": project, "chant.config.ts": "" }, claims: true },
|
|
18
|
+
{ name: "a directory whose dbt project sits one level down", kind: "dbt", files: { "analytics/dbt_project.yml": project }, claims: false },
|
|
19
|
+
{ name: "models without a project file", kind: "dbt", files: { "models/sources.yml": "version: 2\n" }, claims: false },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A sql member's tables and columns as workspace outputs (#3931), end to end:
|
|
3
|
+
* a workspace with a sql member and an app member that links to one column.
|
|
4
|
+
* `chant workspace check` reads the sql member's declarations by folding its
|
|
5
|
+
* source, never running it, so dropping or renaming the column fails the
|
|
6
|
+
* check and names the consumer. Once for Postgres, once for ClickHouse.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { afterAll, afterEach, describe, expect, test, vi } from "vitest";
|
|
14
|
+
import { runDeclarationChecks } from "@intentius/chant/workspace/checks";
|
|
15
|
+
import { runWorkspaceCheck } from "@intentius/chant/workspace/lineage-check";
|
|
16
|
+
import { parseArgs } from "@intentius/chant/cli/main";
|
|
17
|
+
|
|
18
|
+
const scratch: string[] = [];
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
for (const d of scratch) rmSync(d, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function repo(files: Record<string, string>): string {
|
|
27
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-3931-")));
|
|
28
|
+
scratch.push(root);
|
|
29
|
+
execFileSync("git", ["init", "-q"], { cwd: root });
|
|
30
|
+
for (const [path, text] of Object.entries(files)) {
|
|
31
|
+
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
32
|
+
writeFileSync(join(root, path), text);
|
|
33
|
+
}
|
|
34
|
+
return root;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const workspace = (output: string) =>
|
|
38
|
+
JSON.stringify(
|
|
39
|
+
{
|
|
40
|
+
name: "shop",
|
|
41
|
+
schema: 1,
|
|
42
|
+
members: [
|
|
43
|
+
{ name: "db", dir: "db", kind: "chant" },
|
|
44
|
+
{ name: "app", dir: "app", kind: "other", because: "the service that reads the database", links: [{ member: "db", output }] },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
null,
|
|
48
|
+
2,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const config = (dialect: string) => `import type { ChantConfig } from "@intentius/chant";\nexport default { lexicons: ["sql"], sql: { dialect: "${dialect}" } } satisfies ChantConfig;\n`;
|
|
52
|
+
|
|
53
|
+
const postgresOrders = (columns: string) =>
|
|
54
|
+
[
|
|
55
|
+
`import { table } from "@intentius/chant-lexicon-sql/postgres";`,
|
|
56
|
+
``,
|
|
57
|
+
`export const orders = table\``,
|
|
58
|
+
` CREATE TABLE orders (`,
|
|
59
|
+
` id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,`,
|
|
60
|
+
columns,
|
|
61
|
+
` amount numeric(12, 2) NOT NULL`,
|
|
62
|
+
` )\`;`,
|
|
63
|
+
``,
|
|
64
|
+
].join("\n");
|
|
65
|
+
|
|
66
|
+
const clickhouseEvents = (columns: string) =>
|
|
67
|
+
[
|
|
68
|
+
`import { database, table } from "@intentius/chant-lexicon-sql/clickhouse";`,
|
|
69
|
+
``,
|
|
70
|
+
`export const analytics = database\`CREATE DATABASE analytics\`;`,
|
|
71
|
+
`export const events = table\``,
|
|
72
|
+
` CREATE TABLE \${analytics}.events (`,
|
|
73
|
+
` ts DateTime,`,
|
|
74
|
+
columns,
|
|
75
|
+
` kind LowCardinality(String)`,
|
|
76
|
+
` )`,
|
|
77
|
+
` ENGINE = MergeTree`,
|
|
78
|
+
` ORDER BY ts\`;`,
|
|
79
|
+
``,
|
|
80
|
+
].join("\n");
|
|
81
|
+
|
|
82
|
+
function shop(dialect: "postgres" | "clickhouse", output: string, source: string): string {
|
|
83
|
+
return repo({
|
|
84
|
+
"chant.workspace.json": workspace(output),
|
|
85
|
+
"db/chant.config.ts": config(dialect),
|
|
86
|
+
[`db/src/${dialect === "postgres" ? "orders" : "events"}.ts`]: source,
|
|
87
|
+
"app/README.md": "reads the database\n",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Every member of kind other gets WSP009; the app member here is one on purpose. */
|
|
92
|
+
async function check(root: string) {
|
|
93
|
+
const report = await runDeclarationChecks(root);
|
|
94
|
+
return { ...report, diagnostics: report.diagnostics.filter((d) => d.ruleId !== "WSP009") };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function checkExit(root: string): Promise<{ code: number; out: string }> {
|
|
98
|
+
vi.spyOn(process, "cwd").mockReturnValue(root);
|
|
99
|
+
let out = "";
|
|
100
|
+
vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array) => {
|
|
101
|
+
out += String(chunk);
|
|
102
|
+
return true;
|
|
103
|
+
});
|
|
104
|
+
vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
|
|
105
|
+
out += `${args.join(" ")}\n`;
|
|
106
|
+
});
|
|
107
|
+
vi.spyOn(console, "error").mockImplementation((...args: unknown[]) => {
|
|
108
|
+
out += `${args.join(" ")}\n`;
|
|
109
|
+
});
|
|
110
|
+
const code = await runWorkspaceCheck({ args: parseArgs(["workspace", "check"]), plugins: [] } as never);
|
|
111
|
+
return { code, out };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
describe("a sql member's columns are outputs a link can name (#3931)", () => {
|
|
115
|
+
test("Postgres: a link to public.orders.customer_id resolves, and fails check naming the consumer once the column is dropped", async () => {
|
|
116
|
+
const kept = shop("postgres", "public.orders.customer_id", postgresOrders(` customer_id bigint NOT NULL,`));
|
|
117
|
+
const ok = await check(kept);
|
|
118
|
+
expect(ok.diagnostics).toEqual([]);
|
|
119
|
+
expect(ok.links.filter((r) => r.origin === "declared")).toMatchObject([{ consumer: "app", producer: "db", output: "public.orders.customer_id", status: "resolved" }]);
|
|
120
|
+
expect((await checkExit(kept)).code).toBe(0);
|
|
121
|
+
|
|
122
|
+
const dropped = shop("postgres", "public.orders.customer_id", postgresOrders(``));
|
|
123
|
+
const report = await check(dropped);
|
|
124
|
+
expect(report.ok).toBe(false);
|
|
125
|
+
expect(report.diagnostics.map((d) => `${d.ruleId}:${d.entity}`)).toEqual(["WSP093:app"]);
|
|
126
|
+
expect(report.diagnostics[0]!.message).toBe(
|
|
127
|
+
"app's link to db output public.orders.customer_id does not resolve: db has no output public.orders.customer_id; its outputs: public.orders, public.orders.amount, public.orders.id",
|
|
128
|
+
);
|
|
129
|
+
const cli = await checkExit(dropped);
|
|
130
|
+
expect(cli.code).not.toBe(0);
|
|
131
|
+
expect(cli.out).toContain("app's link to db output public.orders.customer_id does not resolve");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("Postgres: a renamed column points the old name at the new one", async () => {
|
|
135
|
+
const root = shop("postgres", "public.orders.customer_id", postgresOrders(` buyer_id bigint NOT NULL, -- previously: customer_id`));
|
|
136
|
+
const [d] = (await check(root)).diagnostics;
|
|
137
|
+
expect(d?.ruleId).toBe("WSP093");
|
|
138
|
+
expect(d?.message).toContain("db has no output public.orders.customer_id; it was renamed to public.orders.buyer_id, so link to that instead");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("Postgres: a quoted mixed-case table is a different output, and the hint names it", async () => {
|
|
142
|
+
const root = shop(
|
|
143
|
+
"postgres",
|
|
144
|
+
"public.orders.customer_id",
|
|
145
|
+
[`import { table } from "@intentius/chant-lexicon-sql/postgres";`, `export const orders = table\`CREATE TABLE "Orders" (customer_id bigint)\`;`, ``].join("\n"),
|
|
146
|
+
);
|
|
147
|
+
const [d] = (await check(root)).diagnostics;
|
|
148
|
+
expect(d?.message).toContain("db has no output public.orders.customer_id; did you mean public.Orders.customer_id? Links match exactly");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("Postgres: a file of tables that does not fold leaves the link unresolved, not missing, and is never run", async () => {
|
|
152
|
+
const root = shop(
|
|
153
|
+
"postgres",
|
|
154
|
+
"public.orders.customer_id",
|
|
155
|
+
[
|
|
156
|
+
`import { table } from "@intentius/chant-lexicon-sql/postgres";`,
|
|
157
|
+
`const name = process.env.ORDERS_TABLE ?? "orders";`,
|
|
158
|
+
`throw new Error("member code ran");`,
|
|
159
|
+
`export const orders = table\`CREATE TABLE \${name} (customer_id bigint)\`;`,
|
|
160
|
+
``,
|
|
161
|
+
].join("\n"),
|
|
162
|
+
);
|
|
163
|
+
const report = await check(root);
|
|
164
|
+
expect(report.ok).toBe(true);
|
|
165
|
+
expect(report.diagnostics.map((d) => d.ruleId)).toEqual(["WSP094"]);
|
|
166
|
+
expect(report.diagnostics[0]!.message).toContain("db/src/orders.ts imports the sql lexicon and does not fold");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("ClickHouse: a link to analytics.events.user_id resolves, and fails check naming the consumer once the column is dropped", async () => {
|
|
170
|
+
const kept = shop("clickhouse", "analytics.events.user_id", clickhouseEvents(` user_id UUID,`));
|
|
171
|
+
const ok = await check(kept);
|
|
172
|
+
expect(ok.diagnostics).toEqual([]);
|
|
173
|
+
expect(ok.links.filter((r) => r.origin === "declared")).toMatchObject([{ consumer: "app", producer: "db", output: "analytics.events.user_id", status: "resolved" }]);
|
|
174
|
+
|
|
175
|
+
const dropped = shop("clickhouse", "analytics.events.user_id", clickhouseEvents(``));
|
|
176
|
+
const report = await check(dropped);
|
|
177
|
+
expect(report.diagnostics.map((d) => `${d.ruleId}:${d.entity}`)).toEqual(["WSP093:app"]);
|
|
178
|
+
expect(report.diagnostics[0]!.message).toBe(
|
|
179
|
+
"app's link to db output analytics.events.user_id does not resolve: db has no output analytics.events.user_id; its outputs: analytics.events, analytics.events.kind, analytics.events.ts",
|
|
180
|
+
);
|
|
181
|
+
const cli = await checkExit(dropped);
|
|
182
|
+
expect(cli.code).not.toBe(0);
|
|
183
|
+
expect(cli.out).toContain("app's link to db output analytics.events.user_id does not resolve");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("files that aren't the member's tables don't hide them (#3941)", () => {
|
|
188
|
+
const op = (name: string) =>
|
|
189
|
+
[
|
|
190
|
+
`import { Op, activity, phase } from "@intentius/chant/op";`,
|
|
191
|
+
``,
|
|
192
|
+
`export const op = Op({ name: "${name}", overview: "${name}", phases: [phase("Run", [activity("run", {})])] });`,
|
|
193
|
+
``,
|
|
194
|
+
].join("\n");
|
|
195
|
+
|
|
196
|
+
test("several Ops exporting `op` in one directory leave the table outputs readable", async () => {
|
|
197
|
+
const root = repo({
|
|
198
|
+
"chant.workspace.json": workspace("public.orders.customer_id"),
|
|
199
|
+
"db/chant.config.ts": config("postgres"),
|
|
200
|
+
"db/src/orders.ts": postgresOrders(` customer_id bigint NOT NULL,`),
|
|
201
|
+
"db/ops/backfill.op.ts": op("backfill"),
|
|
202
|
+
"db/ops/reindex.op.ts": op("reindex"),
|
|
203
|
+
"app/README.md": "reads the database\n",
|
|
204
|
+
});
|
|
205
|
+
const report = await check(root);
|
|
206
|
+
expect(report.diagnostics).toEqual([]);
|
|
207
|
+
expect(report.links.filter((r) => r.origin === "declared")).toMatchObject([{ output: "public.orders.customer_id", status: "resolved" }]);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("with sourceDir set, a sql file outside it that doesn't fold is not read, so a dropped column is missing, not unresolved", async () => {
|
|
211
|
+
const root = repo({
|
|
212
|
+
"chant.workspace.json": workspace("public.orders.customer_id"),
|
|
213
|
+
"db/chant.config.ts": `import type { ChantConfig } from "@intentius/chant";\nexport default { lexicons: ["sql"], sourceDir: "src", sql: { dialect: "postgres" } } satisfies ChantConfig;\n`,
|
|
214
|
+
"db/src/orders.ts": postgresOrders(""),
|
|
215
|
+
"db/scripts/seed.ts": `import { readFileSync } from "node:fs";\nimport { table } from "@intentius/chant-lexicon-sql/postgres";\nexport const seeded = table([readFileSync("seed.sql", "utf8")] as unknown as TemplateStringsArray);\n`,
|
|
216
|
+
"app/README.md": "reads the database\n",
|
|
217
|
+
});
|
|
218
|
+
const report = await check(root);
|
|
219
|
+
expect(report.diagnostics).toMatchObject([{ ruleId: "WSP093" }]);
|
|
220
|
+
expect(report.links.filter((r) => r.origin === "declared")).toMatchObject([{ output: "public.orders.customer_id", status: "missing" }]);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { database, table as chTable, view as chView } from "./clickhouse/entities";
|
|
3
|
+
import { schema, table, view } from "./postgres/entities";
|
|
4
|
+
import { sqlWorkspaceOutputs } from "./workspace-outputs";
|
|
5
|
+
|
|
6
|
+
const names = (entities: Record<string, unknown>) => sqlWorkspaceOutputs(new Map(Object.entries(entities)) as never)?.outputs.map((o) => o.name);
|
|
7
|
+
|
|
8
|
+
describe("sqlWorkspaceOutputs: Postgres (#3931)", () => {
|
|
9
|
+
test("an unqualified table is in public, and an unquoted name folds to lower case", () => {
|
|
10
|
+
expect(names({ orders: table`CREATE TABLE Orders (Customer_ID bigint, amount numeric)` })).toEqual([
|
|
11
|
+
"public.orders",
|
|
12
|
+
"public.orders.amount",
|
|
13
|
+
"public.orders.customer_id",
|
|
14
|
+
]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("a quoted name keeps its case and is a different output", () => {
|
|
18
|
+
expect(names({ orders: table`CREATE TABLE "Orders" ("CustomerId" bigint)` })).toEqual(["public.Orders", "public.Orders.CustomerId"]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("a declared schema qualifies the name", () => {
|
|
22
|
+
const app = schema`CREATE SCHEMA app`;
|
|
23
|
+
expect(names({ app, users: table`CREATE TABLE ${app}.users (id bigint)` })).toEqual(["app.users", "app.users.id"]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("a view's columns come from its select list; a * is a gap", () => {
|
|
27
|
+
const orders = table`CREATE TABLE orders (id bigint, customer_id bigint, amount numeric)`;
|
|
28
|
+
const totals = view`CREATE VIEW totals AS SELECT customer_id, sum(amount) AS total FROM ${orders} GROUP BY customer_id`;
|
|
29
|
+
expect(names({ orders, totals })).toContain("public.totals.total");
|
|
30
|
+
expect(names({ orders, totals })).toContain("public.totals.customer_id");
|
|
31
|
+
|
|
32
|
+
const all = view`CREATE MATERIALIZED VIEW everything AS SELECT * FROM ${orders}`;
|
|
33
|
+
const report = sqlWorkspaceOutputs(new Map<string, unknown>([["orders", orders], ["all", all]]) as never)!;
|
|
34
|
+
expect(report.outputs.map((o) => o.name)).toContain("public.everything");
|
|
35
|
+
expect(report.gaps?.join("\n")).toContain("view public.everything (all)");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("a renamed column reports its old name", () => {
|
|
39
|
+
const report = sqlWorkspaceOutputs(new Map([["orders", table`CREATE TABLE orders (
|
|
40
|
+
buyer_id bigint -- previously: customer_id
|
|
41
|
+
)`]]) as never)!;
|
|
42
|
+
expect(report.outputs.find((o) => o.name === "public.orders.buyer_id")?.previously).toEqual(["public.orders.customer_id"]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("a file importing the lexicon that did not fold is a gap; other files are not", () => {
|
|
46
|
+
const report = sqlWorkspaceOutputs(new Map(), [
|
|
47
|
+
{ file: "db/src/a.ts", text: `import { table } from "@intentius/chant-lexicon-sql/postgres";`, reason: "it reads process.env" },
|
|
48
|
+
{ file: "db/src/b.ts", text: `export const x = 1;`, reason: "it does not fold" },
|
|
49
|
+
])!;
|
|
50
|
+
expect(report.outputs).toEqual([]);
|
|
51
|
+
expect(report.gaps).toEqual(["db/src/a.ts imports the sql lexicon and does not fold, so the tables it declares were not read: it reads process.env"]);
|
|
52
|
+
expect(sqlWorkspaceOutputs(new Map(), [{ file: "b.ts", text: "", reason: "x" }])).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("sqlWorkspaceOutputs: ClickHouse (#3931)", () => {
|
|
57
|
+
test("an unqualified table is in default, and names keep their case", () => {
|
|
58
|
+
expect(names({ events: chTable`CREATE TABLE Events (UserId UUID) ENGINE = MergeTree ORDER BY UserId` })).toEqual(["default.Events", "default.Events.UserId"]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("a declared database qualifies the name; an aliased view's columns are read", () => {
|
|
62
|
+
const analytics = database`CREATE DATABASE analytics`;
|
|
63
|
+
const events = chTable`CREATE TABLE ${analytics}.events (ts DateTime, user_id UUID) ENGINE = MergeTree ORDER BY ts`;
|
|
64
|
+
const daily = chView`CREATE VIEW ${analytics}.daily AS SELECT toDate(ts) AS day, count() AS n FROM ${events} GROUP BY day`;
|
|
65
|
+
expect(names({ analytics, events, daily })).toEqual([
|
|
66
|
+
"analytics.daily",
|
|
67
|
+
"analytics.daily.day",
|
|
68
|
+
"analytics.daily.n",
|
|
69
|
+
"analytics.events",
|
|
70
|
+
"analytics.events.ts",
|
|
71
|
+
"analytics.events.user_id",
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("an unaliased expression in a view's select list is a gap", () => {
|
|
76
|
+
const events = chTable`CREATE TABLE events (ts DateTime) ENGINE = MergeTree ORDER BY ts`;
|
|
77
|
+
const mv = chView`CREATE MATERIALIZED VIEW counts ENGINE = MergeTree ORDER BY tuple() AS SELECT count() FROM ${events}`;
|
|
78
|
+
const report = sqlWorkspaceOutputs(new Map<string, unknown>([["events", events], ["mv", mv]]) as never)!;
|
|
79
|
+
expect(report.outputs.map((o) => o.name)).toContain("default.counts");
|
|
80
|
+
expect(report.gaps?.join("\n")).toContain("view default.counts (mv)");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workspace outputs a sql member exposes (#3931): each table and view,
|
|
3
|
+
* and each of their columns, as names another member's link may target.
|
|
4
|
+
*
|
|
5
|
+
* | Dialect | Relation | Column |
|
|
6
|
+
* |---|---|---|
|
|
7
|
+
* | Postgres | `<schema>.<table>` | `<schema>.<table>.<column>` |
|
|
8
|
+
* | ClickHouse | `<database>.<table>` | `<database>.<table>.<column>` |
|
|
9
|
+
*
|
|
10
|
+
* The schema is `public` when the DDL does not qualify the name, and the
|
|
11
|
+
* database `default`. Views and materialized views are relations too.
|
|
12
|
+
*
|
|
13
|
+
* Names are the declared ones, read from the entities the member's source
|
|
14
|
+
* folds to, never an environment's or a profile's rendering: a member that
|
|
15
|
+
* renames its database per environment or tenant at deploy time exposes the
|
|
16
|
+
* same outputs everywhere.
|
|
17
|
+
*
|
|
18
|
+
* Identifiers are written unquoted, in the case the dialect keeps:
|
|
19
|
+
*
|
|
20
|
+
* - Postgres folds an unquoted identifier to lower case and keeps a quoted
|
|
21
|
+
* one exactly, so `CREATE TABLE Orders` is `public.orders` and `CREATE
|
|
22
|
+
* TABLE "Orders"` is `public.Orders`, a different output;
|
|
23
|
+
* - ClickHouse is case-sensitive, so a name is kept as written, quoted or not.
|
|
24
|
+
*
|
|
25
|
+
* A link's near-name hint compares names case-insensitively, so a link to
|
|
26
|
+
* `public.orders` when only `public.Orders` exists is pointed at it.
|
|
27
|
+
*
|
|
28
|
+
* A view's columns are its declared column list, or else the names of its
|
|
29
|
+
* select list (an alias, or the column a bare reference reads). A select
|
|
30
|
+
* list item with no name chant can derive, or a `*`, leaves the view's
|
|
31
|
+
* columns incomplete: that is a gap, so a link to one of them is unresolved
|
|
32
|
+
* rather than missing.
|
|
33
|
+
*
|
|
34
|
+
* A `-- previously: <old>` rename hint, on a column's line or before the
|
|
35
|
+
* CREATE, is reported as the output's previous name, so a link to the old
|
|
36
|
+
* name says where it went.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
40
|
+
import type { LexiconUnreadSource, LexiconWorkspaceOutput, LexiconWorkspaceOutputs } from "@intentius/chant/graph-ir";
|
|
41
|
+
import { CLICKHOUSE_ENTITY_TYPES, isClickHouseObject, type ColumnDef as ClickHouseColumnDef, type TableProps as ClickHouseTableProps, type ViewProps as ClickHouseViewProps } from "./clickhouse/entities";
|
|
42
|
+
import { canonicalObject } from "./clickhouse/plan/normalize";
|
|
43
|
+
import { isPostgresObject, POSTGRES_ENTITY_TYPES, type TableProps as PostgresTableProps, type ViewProps as PostgresViewProps } from "./postgres/entities";
|
|
44
|
+
import { renameHints } from "./postgres/plan/schema";
|
|
45
|
+
import type { LineageEdge } from "./core/references";
|
|
46
|
+
|
|
47
|
+
/** The schema an unqualified Postgres name is created in. */
|
|
48
|
+
export const POSTGRES_DEFAULT_SCHEMA = "public";
|
|
49
|
+
/** The database an unqualified ClickHouse name is created in. */
|
|
50
|
+
export const CLICKHOUSE_DEFAULT_DATABASE = "default";
|
|
51
|
+
|
|
52
|
+
/** A module the sql lexicon's declarations import from. */
|
|
53
|
+
const SQL_MODULE = /from\s+["']@intentius\/chant-lexicon-sql(\/(postgres|clickhouse|sql-files))?["']/;
|
|
54
|
+
|
|
55
|
+
interface Collected {
|
|
56
|
+
outputs: Map<string, LexiconWorkspaceOutput>;
|
|
57
|
+
gaps: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function add(c: Collected, name: string, entity: string, previously: readonly string[] = []): void {
|
|
61
|
+
const prev = c.outputs.get(name);
|
|
62
|
+
const before = [...new Set([...(prev?.previously ?? []), ...previously.filter((p) => p !== name)])].sort();
|
|
63
|
+
c.outputs.set(name, { name, entity: prev?.entity ?? entity, ...(before.length ? { previously: before } : {}) });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A relation and its columns, with the rename hints that apply. */
|
|
67
|
+
function addRelation(
|
|
68
|
+
c: Collected,
|
|
69
|
+
key: string,
|
|
70
|
+
qualifier: string,
|
|
71
|
+
name: string,
|
|
72
|
+
columns: readonly string[],
|
|
73
|
+
hints: { previously?: string; columns: Record<string, string> },
|
|
74
|
+
): void {
|
|
75
|
+
const relation = `${qualifier}.${name}`;
|
|
76
|
+
// A relation's hint names its old name, bare or qualified.
|
|
77
|
+
const oldRelation = hints.previously ? (hints.previously.includes(".") ? hints.previously : `${qualifier}.${hints.previously}`) : undefined;
|
|
78
|
+
add(c, relation, key, oldRelation ? [oldRelation] : []);
|
|
79
|
+
for (const col of columns) {
|
|
80
|
+
const prevCol = hints.columns[col];
|
|
81
|
+
const previously = [
|
|
82
|
+
...(prevCol ? [`${relation}.${prevCol}`] : []),
|
|
83
|
+
...(oldRelation ? [`${oldRelation}.${col}`, ...(prevCol ? [`${oldRelation}.${prevCol}`] : [])] : []),
|
|
84
|
+
];
|
|
85
|
+
add(c, `${relation}.${col}`, key, previously);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A view's output columns from its select list, and whether they are all known. */
|
|
90
|
+
function lineageColumns(lineage: readonly LineageEdge[], unnamed: (output: string, index: number) => boolean): { columns: string[]; complete: boolean } {
|
|
91
|
+
const columns: string[] = [];
|
|
92
|
+
let complete = true;
|
|
93
|
+
lineage.forEach((e, i) => {
|
|
94
|
+
if (/(^|\.)\s*\*$/.test(e.expr.trim()) || unnamed(e.output, i + 1)) complete = false;
|
|
95
|
+
else columns.push(e.output);
|
|
96
|
+
});
|
|
97
|
+
return { columns, complete };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function postgres(c: Collected, key: string, e: Declarable): void {
|
|
101
|
+
const type = e.entityType;
|
|
102
|
+
if (type === POSTGRES_ENTITY_TYPES.table) {
|
|
103
|
+
const props = (e as unknown as { props: PostgresTableProps }).props;
|
|
104
|
+
const schema = props.schema ?? POSTGRES_DEFAULT_SCHEMA;
|
|
105
|
+
const columns = props.columns.map((col) => col.name);
|
|
106
|
+
const inherited = (parent: unknown): string[] | undefined =>
|
|
107
|
+
isPostgresObject(parent) && parent.entityType === POSTGRES_ENTITY_TYPES.table ? (parent.props as PostgresTableProps).columns.map((col) => col.name) : undefined;
|
|
108
|
+
// A partition takes its parent's columns; INHERITS adds the parents'.
|
|
109
|
+
if (props.partitionOf !== undefined) {
|
|
110
|
+
const fromParent = inherited(props.partitionOf);
|
|
111
|
+
if (fromParent) columns.push(...fromParent.filter((n) => !columns.includes(n)));
|
|
112
|
+
else c.gaps.push(`table ${schema}.${props.name} (${key}) is a partition of ${String(props.partitionOf)}, which this member does not declare, so its columns are not known`);
|
|
113
|
+
}
|
|
114
|
+
for (const parent of props.inherits) {
|
|
115
|
+
const fromParent = inherited(parent);
|
|
116
|
+
if (fromParent) columns.push(...fromParent.filter((n) => !columns.includes(n)));
|
|
117
|
+
else c.gaps.push(`table ${schema}.${props.name} (${key}) inherits ${String(parent)}, which this member does not declare, so its columns are not all known`);
|
|
118
|
+
}
|
|
119
|
+
if (props.ofType !== undefined) {
|
|
120
|
+
const attrs = isPostgresObject(props.ofType) ? (props.ofType.props as { attributes?: { name: string }[] }).attributes : undefined;
|
|
121
|
+
if (attrs) columns.push(...attrs.map((a) => a.name).filter((n) => !columns.includes(n)));
|
|
122
|
+
else c.gaps.push(`table ${schema}.${props.name} (${key}) is OF ${String(props.ofType)}, which this member does not declare, so its columns are not known`);
|
|
123
|
+
}
|
|
124
|
+
if (props.like.length > 0) c.gaps.push(`table ${schema}.${props.name} (${key}) copies columns with LIKE, which are not read`);
|
|
125
|
+
addRelation(c, key, schema, props.name, columns, renameHints(props.ddl));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (type === POSTGRES_ENTITY_TYPES.view || type === POSTGRES_ENTITY_TYPES.materializedView) {
|
|
129
|
+
const props = (e as unknown as { props: PostgresViewProps }).props;
|
|
130
|
+
const schema = props.schema ?? POSTGRES_DEFAULT_SCHEMA;
|
|
131
|
+
let columns = [...props.columns];
|
|
132
|
+
if (columns.length === 0) {
|
|
133
|
+
const derived = lineageColumns(props.lineage, (output) => output === "?column?");
|
|
134
|
+
columns = derived.columns;
|
|
135
|
+
if (!derived.complete) c.gaps.push(`view ${schema}.${props.name} (${key}) has a select list item whose name can't be read without a server (a * or an unnamed expression), so its columns are not all known`);
|
|
136
|
+
}
|
|
137
|
+
addRelation(c, key, schema, props.name, columns, renameHints(props.ddl));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function clickhouseHints(ddl: string): { previously?: string; columns: Record<string, string> } {
|
|
142
|
+
try {
|
|
143
|
+
const o = canonicalObject(ddl, CLICKHOUSE_DEFAULT_DATABASE);
|
|
144
|
+
const columns: Record<string, string> = {};
|
|
145
|
+
for (const col of o.columns) if (col.previously) columns[col.name] = col.previously;
|
|
146
|
+
return { ...(o.previously ? { previously: o.previously } : {}), columns };
|
|
147
|
+
} catch {
|
|
148
|
+
return { columns: {} };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function clickhouse(c: Collected, key: string, e: Declarable): void {
|
|
153
|
+
const type = e.entityType;
|
|
154
|
+
if (type === CLICKHOUSE_ENTITY_TYPES.table) {
|
|
155
|
+
const props = (e as unknown as { props: ClickHouseTableProps }).props;
|
|
156
|
+
const database = props.database ?? CLICKHOUSE_DEFAULT_DATABASE;
|
|
157
|
+
if (props.columns.length === 0) c.gaps.push(`table ${database}.${props.name} (${key}) declares no columns, so the server's are not known`);
|
|
158
|
+
addRelation(c, key, database, props.name, props.columns.map((col: ClickHouseColumnDef) => col.name), clickhouseHints(props.ddl));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (type === CLICKHOUSE_ENTITY_TYPES.view || type === CLICKHOUSE_ENTITY_TYPES.materializedView) {
|
|
162
|
+
const props = (e as unknown as { props: ClickHouseViewProps }).props;
|
|
163
|
+
const database = props.database ?? CLICKHOUSE_DEFAULT_DATABASE;
|
|
164
|
+
let columns = props.columns.map((col: ClickHouseColumnDef) => col.name);
|
|
165
|
+
if (columns.length === 0) {
|
|
166
|
+
const derived = lineageColumns(props.lineage, (output, n) => output === `_${n}`);
|
|
167
|
+
columns = derived.columns;
|
|
168
|
+
if (!derived.complete) c.gaps.push(`view ${database}.${props.name} (${key}) has a select list item whose name can't be read without a server (a * or an unaliased expression), so its columns are not all known`);
|
|
169
|
+
}
|
|
170
|
+
addRelation(c, key, database, props.name, columns, clickhouseHints(props.ddl));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The `workspaceOutputs` hook: every table and view the entities declare,
|
|
176
|
+
* and their columns. `unread` names the member's files that did not fold;
|
|
177
|
+
* one that imports the sql lexicon may declare tables, so it is a gap.
|
|
178
|
+
*/
|
|
179
|
+
export function sqlWorkspaceOutputs(entities: ReadonlyMap<string, Declarable>, unread: readonly LexiconUnreadSource[] = []): LexiconWorkspaceOutputs | undefined {
|
|
180
|
+
const c: Collected = { outputs: new Map(), gaps: [] };
|
|
181
|
+
let any = false;
|
|
182
|
+
for (const [key, e] of entities) {
|
|
183
|
+
if (isPostgresObject(e)) {
|
|
184
|
+
any = true;
|
|
185
|
+
postgres(c, key, e);
|
|
186
|
+
} else if (isClickHouseObject(e)) {
|
|
187
|
+
any = true;
|
|
188
|
+
clickhouse(c, key, e);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const u of unread) {
|
|
192
|
+
if (!SQL_MODULE.test(u.text)) continue;
|
|
193
|
+
any = true;
|
|
194
|
+
c.gaps.push(`${u.file} imports the sql lexicon and does not fold, so the tables it declares were not read: ${u.reason}`);
|
|
195
|
+
}
|
|
196
|
+
if (!any) return undefined;
|
|
197
|
+
const outputs = [...c.outputs.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
198
|
+
return { outputs, ...(c.gaps.length ? { gaps: c.gaps } : {}) };
|
|
199
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Member kinds this lexicon supplies to a chant workspace (#3932). chant reads this file as data and never imports the lexicon. A dbt project reads the tables a sql member declares, so its kind ships beside them. Its reader, dbt-project, is chant's own code: it parses dbt_project.yml and the sources YAML under the model paths, never runs dbt, never renders Jinja and never reads profiles.yml, and turns each source table and column into a link to the sql member's <schema>.<table> and <schema>.<table>.<column> outputs (#3931).",
|
|
3
|
+
"schema": 1,
|
|
4
|
+
"kinds": [
|
|
5
|
+
{
|
|
6
|
+
"name": "dbt",
|
|
7
|
+
"description": "a dbt project, with a dbt_project.yml in its directory; its sources link to the tables and columns a sql member declares, and dbt is never run",
|
|
8
|
+
"precedence": 300,
|
|
9
|
+
"probe": {
|
|
10
|
+
"anyFile": [
|
|
11
|
+
"dbt_project.yml"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"reader": "dbt-project",
|
|
15
|
+
"fields": {
|
|
16
|
+
"sqlMember": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "The member whose tables the project's sources read. Unset, each source table links to the one member that exposes it, and a table two members expose is ambiguous (WSP095).",
|
|
19
|
+
"pattern": "^[a-z0-9][a-z0-9-]{0,39}$"
|
|
20
|
+
},
|
|
21
|
+
"dialect": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "How source names are cased before they are matched: postgres folds an unquoted name to lowercase and keeps a quoted one as written, the way dbt-postgres quotes a source's schema and identifier unless quoting turns it off; clickhouse keeps every name as written.",
|
|
24
|
+
"default": "postgres",
|
|
25
|
+
"pattern": "^(postgres|clickhouse)$"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|