@intentius/chant-lexicon-sql 0.116.0 → 0.118.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/dist/clickhouse/access.d.ts +132 -0
  2. package/dist/clickhouse/access.d.ts.map +1 -0
  3. package/dist/clickhouse/apply/apply.d.ts.map +1 -1
  4. package/dist/clickhouse/apply/statements.d.ts +2 -0
  5. package/dist/clickhouse/apply/statements.d.ts.map +1 -1
  6. package/dist/clickhouse/entities.d.ts +171 -2
  7. package/dist/clickhouse/entities.d.ts.map +1 -1
  8. package/dist/clickhouse/http.d.ts +3 -0
  9. package/dist/clickhouse/http.d.ts.map +1 -1
  10. package/dist/clickhouse/import/generator.d.ts +28 -0
  11. package/dist/clickhouse/import/generator.d.ts.map +1 -1
  12. package/dist/clickhouse/import/ir.d.ts.map +1 -1
  13. package/dist/clickhouse/import/parser.d.ts +3 -3
  14. package/dist/clickhouse/import/parser.d.ts.map +1 -1
  15. package/dist/clickhouse/live/bind.d.ts.map +1 -1
  16. package/dist/clickhouse/live/catalog.d.ts +41 -6
  17. package/dist/clickhouse/live/catalog.d.ts.map +1 -1
  18. package/dist/clickhouse/live/describe-resources.d.ts +3 -2
  19. package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
  20. package/dist/clickhouse/parser.d.ts +54 -3
  21. package/dist/clickhouse/parser.d.ts.map +1 -1
  22. package/dist/clickhouse/plan/commands.d.ts +3 -1
  23. package/dist/clickhouse/plan/commands.d.ts.map +1 -1
  24. package/dist/clickhouse/plan/deep.d.ts.map +1 -1
  25. package/dist/clickhouse/plan/diff.d.ts +12 -0
  26. package/dist/clickhouse/plan/diff.d.ts.map +1 -1
  27. package/dist/clickhouse/plan/normalize.d.ts +61 -1
  28. package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
  29. package/dist/clickhouse/plan/report.d.ts.map +1 -1
  30. package/dist/clickhouse/plan/rules.d.ts +7 -0
  31. package/dist/clickhouse/plan/rules.d.ts.map +1 -1
  32. package/dist/clickhouse/plan/schema.d.ts +8 -4
  33. package/dist/clickhouse/plan/schema.d.ts.map +1 -1
  34. package/dist/clickhouse/rebuild/backfill.d.ts +3 -1
  35. package/dist/clickhouse/rebuild/backfill.d.ts.map +1 -1
  36. package/dist/clickhouse/rebuild/observe.d.ts +7 -3
  37. package/dist/clickhouse/rebuild/observe.d.ts.map +1 -1
  38. package/dist/clickhouse/rebuild/op.d.ts +8 -0
  39. package/dist/clickhouse/rebuild/op.d.ts.map +1 -1
  40. package/dist/clickhouse/rebuild/steps.d.ts +48 -0
  41. package/dist/clickhouse/rebuild/steps.d.ts.map +1 -1
  42. package/dist/clickhouse/testing/fake-server.d.ts.map +1 -1
  43. package/dist/clickhouse/testing/writable-server.d.ts.map +1 -1
  44. package/dist/clickhouse/topology.d.ts.map +1 -1
  45. package/dist/clickhouse.d.ts +1 -1
  46. package/dist/clickhouse.d.ts.map +1 -1
  47. package/dist/codegen/classifier-page.d.ts.map +1 -1
  48. package/dist/codegen/registry.d.ts.map +1 -1
  49. package/dist/config.d.ts +70 -7
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/core/references.d.ts +4 -2
  52. package/dist/core/references.d.ts.map +1 -1
  53. package/dist/core/statements.d.ts +11 -0
  54. package/dist/core/statements.d.ts.map +1 -1
  55. package/dist/files/build-root.d.ts +26 -0
  56. package/dist/files/build-root.d.ts.map +1 -0
  57. package/dist/files/clickhouse.d.ts +19 -0
  58. package/dist/files/clickhouse.d.ts.map +1 -0
  59. package/dist/files/common.d.ts +25 -0
  60. package/dist/files/common.d.ts.map +1 -0
  61. package/dist/files/postgres.d.ts +36 -0
  62. package/dist/files/postgres.d.ts.map +1 -0
  63. package/dist/import-parser.d.ts +25 -0
  64. package/dist/import-parser.d.ts.map +1 -0
  65. package/dist/index.d.ts +2 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/integrity.json +8 -8
  68. package/dist/lint/rules/sqlch001.d.ts +1 -1
  69. package/dist/lint/rules/sqlch001.d.ts.map +1 -1
  70. package/dist/lint/rules/sqlch002.d.ts.map +1 -1
  71. package/dist/lint/rules/sqlpg001.d.ts.map +1 -1
  72. package/dist/lint/rules/templates.d.ts +1 -1
  73. package/dist/lint/rules/templates.d.ts.map +1 -1
  74. package/dist/lsp/postgres-template.d.ts.map +1 -1
  75. package/dist/manifest.json +1 -1
  76. package/dist/meta.json +50 -0
  77. package/dist/okf/index.md +11 -1
  78. package/dist/okf/rules/SQLCH001.md +2 -2
  79. package/dist/okf/types/Dictionary.md +9 -0
  80. package/dist/okf/types/Function.md +9 -0
  81. package/dist/okf/types/Grant.md +9 -0
  82. package/dist/okf/types/PostgresDefaultPrivileges.md +9 -0
  83. package/dist/okf/types/PostgresGrant.md +9 -0
  84. package/dist/okf/types/PostgresPolicy.md +9 -0
  85. package/dist/okf/types/PostgresRole.md +9 -0
  86. package/dist/okf/types/Role.md +9 -0
  87. package/dist/okf/types/RowPolicy.md +9 -0
  88. package/dist/okf/types/User.md +9 -0
  89. package/dist/op/activities/clickhouse-rebuild.d.ts.map +1 -1
  90. package/dist/op/activities/postgres-migration.d.ts.map +1 -1
  91. package/dist/op/activity-contracts.d.ts +19 -0
  92. package/dist/op/activity-contracts.d.ts.map +1 -1
  93. package/dist/plugin.d.ts.map +1 -1
  94. package/dist/postgres/access/acl.d.ts +119 -0
  95. package/dist/postgres/access/acl.d.ts.map +1 -0
  96. package/dist/postgres/access/import.d.ts +13 -0
  97. package/dist/postgres/access/import.d.ts.map +1 -0
  98. package/dist/postgres/access/live.d.ts +24 -0
  99. package/dist/postgres/access/live.d.ts.map +1 -0
  100. package/dist/postgres/apply/apply.d.ts.map +1 -1
  101. package/dist/postgres/apply/statements.d.ts +71 -0
  102. package/dist/postgres/apply/statements.d.ts.map +1 -1
  103. package/dist/postgres/entities.d.ts +106 -4
  104. package/dist/postgres/entities.d.ts.map +1 -1
  105. package/dist/postgres/entity-types.d.ts +4 -0
  106. package/dist/postgres/entity-types.d.ts.map +1 -1
  107. package/dist/postgres/import/generator.d.ts +37 -1
  108. package/dist/postgres/import/generator.d.ts.map +1 -1
  109. package/dist/postgres/import/live-export.d.ts +6 -0
  110. package/dist/postgres/import/live-export.d.ts.map +1 -1
  111. package/dist/postgres/live/bind.d.ts +2 -0
  112. package/dist/postgres/live/bind.d.ts.map +1 -1
  113. package/dist/postgres/live/catalog.d.ts +11 -2
  114. package/dist/postgres/live/catalog.d.ts.map +1 -1
  115. package/dist/postgres/live/client.d.ts +3 -0
  116. package/dist/postgres/live/client.d.ts.map +1 -1
  117. package/dist/postgres/live/describe-resources.d.ts +9 -0
  118. package/dist/postgres/live/describe-resources.d.ts.map +1 -1
  119. package/dist/postgres/migrate/op.d.ts +29 -0
  120. package/dist/postgres/migrate/op.d.ts.map +1 -1
  121. package/dist/postgres/parser.d.ts +76 -3
  122. package/dist/postgres/parser.d.ts.map +1 -1
  123. package/dist/postgres/plan/commands.d.ts +12 -0
  124. package/dist/postgres/plan/commands.d.ts.map +1 -1
  125. package/dist/postgres/plan/deep.d.ts.map +1 -1
  126. package/dist/postgres/plan/diff.d.ts +10 -1
  127. package/dist/postgres/plan/diff.d.ts.map +1 -1
  128. package/dist/postgres/plan/normalize.d.ts +8 -1
  129. package/dist/postgres/plan/normalize.d.ts.map +1 -1
  130. package/dist/postgres/plan/rules.d.ts +8 -0
  131. package/dist/postgres/plan/rules.d.ts.map +1 -1
  132. package/dist/postgres/plan/schema.d.ts +2 -1
  133. package/dist/postgres/plan/schema.d.ts.map +1 -1
  134. package/dist/postgres/plan/server-normalize.d.ts +4 -1
  135. package/dist/postgres/plan/server-normalize.d.ts.map +1 -1
  136. package/dist/postgres.d.ts +1 -1
  137. package/dist/postgres.d.ts.map +1 -1
  138. package/dist/rules/postgres-templates.ts +2 -2
  139. package/dist/rules/sqlch001.ts +26 -4
  140. package/dist/rules/sqlch002.ts +3 -2
  141. package/dist/rules/sqlpg001.ts +7 -4
  142. package/dist/rules/templates.ts +2 -2
  143. package/dist/serializer.d.ts +2 -1
  144. package/dist/serializer.d.ts.map +1 -1
  145. package/dist/sql-files.d.ts +79 -0
  146. package/dist/sql-files.d.ts.map +1 -0
  147. package/dist/token-source.d.ts +88 -0
  148. package/dist/token-source.d.ts.map +1 -0
  149. package/dist/validate.d.ts.map +1 -1
  150. package/package.json +2 -2
  151. package/src/clickhouse/access.test.ts +203 -0
  152. package/src/clickhouse/access.ts +595 -0
  153. package/src/clickhouse/apply/apply.ts +29 -20
  154. package/src/clickhouse/apply/statements.ts +105 -8
  155. package/src/clickhouse/dictionary.test.ts +177 -0
  156. package/src/clickhouse/entities.ts +356 -6
  157. package/src/clickhouse/function.test.ts +91 -0
  158. package/src/clickhouse/http.ts +11 -2
  159. package/src/clickhouse/import/generator.ts +81 -46
  160. package/src/clickhouse/import/import.test.ts +9 -3
  161. package/src/clickhouse/import/ir.ts +6 -2
  162. package/src/clickhouse/import/parser.ts +10 -18
  163. package/src/clickhouse/live/access.e2e.test.ts +218 -0
  164. package/src/clickhouse/live/bind.ts +17 -6
  165. package/src/clickhouse/live/catalog.ts +106 -10
  166. package/src/clickhouse/live/describe-resources.ts +23 -10
  167. package/src/clickhouse/live/dictionaries.e2e.test.ts +186 -0
  168. package/src/clickhouse/live/functions.e2e.test.ts +155 -0
  169. package/src/clickhouse/live/live.test.ts +2 -2
  170. package/src/clickhouse/parser.ts +136 -4
  171. package/src/clickhouse/plan/commands.ts +18 -7
  172. package/src/clickhouse/plan/deep.ts +71 -11
  173. package/src/clickhouse/plan/diff.ts +65 -2
  174. package/src/clickhouse/plan/normalize.ts +200 -6
  175. package/src/clickhouse/plan/report.ts +14 -3
  176. package/src/clickhouse/plan/rules.ts +44 -1
  177. package/src/clickhouse/plan/schema.ts +40 -17
  178. package/src/clickhouse/plan/server-format.ts +1 -1
  179. package/src/clickhouse/rebuild/backfill.ts +5 -25
  180. package/src/clickhouse/rebuild/observe.ts +7 -3
  181. package/src/clickhouse/rebuild/op.ts +9 -0
  182. package/src/clickhouse/rebuild/rebuild.e2e.test.ts +62 -0
  183. package/src/clickhouse/rebuild/rebuild.test.ts +54 -0
  184. package/src/clickhouse/rebuild/steps.ts +136 -1
  185. package/src/clickhouse/testing/fake-server.ts +2 -0
  186. package/src/clickhouse/testing/writable-server.ts +2 -0
  187. package/src/clickhouse/topology.ts +17 -2
  188. package/src/clickhouse.ts +19 -0
  189. package/src/codegen/classifier-page.ts +2 -0
  190. package/src/codegen/registry.ts +10 -0
  191. package/src/config.ts +22 -3
  192. package/src/core/references.ts +5 -3
  193. package/src/core/statements.ts +11 -1
  194. package/src/files/build-root.ts +62 -0
  195. package/src/files/clickhouse.ts +52 -0
  196. package/src/files/common.ts +54 -0
  197. package/src/files/postgres.ts +371 -0
  198. package/src/generated/lexicon-sql.json +50 -0
  199. package/src/import-parser.ts +61 -0
  200. package/src/index.ts +14 -1
  201. package/src/lint/rules/postgres-templates.ts +2 -2
  202. package/src/lint/rules/sqlch001.ts +26 -4
  203. package/src/lint/rules/sqlch002.ts +3 -2
  204. package/src/lint/rules/sqlpg001.ts +7 -4
  205. package/src/lint/rules/templates.ts +2 -2
  206. package/src/lsp/postgres-template.ts +2 -0
  207. package/src/lsp/template.ts +3 -3
  208. package/src/mcp/index.ts +6 -6
  209. package/src/mcp/postgres.ts +1 -1
  210. package/src/migration-statements.test.ts +25 -0
  211. package/src/migration-statements.ts +8 -0
  212. package/src/op/activities/clickhouse-rebuild.ts +1 -0
  213. package/src/op/activities/postgres-migration.ts +27 -2
  214. package/src/op/activity-contracts.ts +2 -0
  215. package/src/plugin.ts +29 -5
  216. package/src/postgres/access/access.e2e.test.ts +191 -0
  217. package/src/postgres/access/access.test.ts +147 -0
  218. package/src/postgres/access/acl.ts +400 -0
  219. package/src/postgres/access/import.ts +64 -0
  220. package/src/postgres/access/live.ts +123 -0
  221. package/src/postgres/apply/apply.ts +117 -4
  222. package/src/postgres/apply/constraint-sequences.e2e.test.ts +179 -0
  223. package/src/postgres/apply/safe-sequences.e2e.test.ts +176 -0
  224. package/src/postgres/apply/statements.test.ts +138 -6
  225. package/src/postgres/apply/statements.ts +210 -18
  226. package/src/postgres/entities.ts +287 -11
  227. package/src/postgres/entity-types.ts +4 -0
  228. package/src/postgres/import/generator.ts +129 -46
  229. package/src/postgres/import/live-export.ts +12 -2
  230. package/src/postgres/live/bind.ts +20 -6
  231. package/src/postgres/live/catalog.ts +79 -3
  232. package/src/postgres/live/client.ts +10 -2
  233. package/src/postgres/live/describe-resources.ts +27 -3
  234. package/src/postgres/migrate/migrate.test.ts +24 -1
  235. package/src/postgres/migrate/op.ts +80 -35
  236. package/src/postgres/migrate/outer.e2e.test.ts +163 -0
  237. package/src/postgres/parser.ts +247 -8
  238. package/src/postgres/plan/commands.ts +60 -7
  239. package/src/postgres/plan/deep.ts +24 -3
  240. package/src/postgres/plan/diff.ts +71 -6
  241. package/src/postgres/plan/normalize.ts +115 -2
  242. package/src/postgres/plan/plan.test.ts +2 -2
  243. package/src/postgres/plan/rules.ts +49 -5
  244. package/src/postgres/plan/schema.ts +4 -2
  245. package/src/postgres/plan/server-normalize.ts +29 -1
  246. package/src/postgres.ts +11 -0
  247. package/src/serializer.ts +14 -9
  248. package/src/skill-defs.ts +1 -1
  249. package/src/skills/chant-sql-postgres-migration.md +3 -1
  250. package/src/skills/chant-sql-postgres-plan.md +3 -3
  251. package/src/skills/chant-sql-postgres.md +9 -2
  252. package/src/skills/chant-sql-rebuild.md +3 -2
  253. package/src/skills/chant-sql.md +7 -1
  254. package/src/sql-files.test.ts +188 -0
  255. package/src/sql-files.ts +218 -0
  256. package/src/token-source.e2e.test.ts +142 -0
  257. package/src/token-source.test.ts +119 -0
  258. package/src/token-source.ts +189 -0
  259. package/src/validate.ts +10 -0
@@ -8,7 +8,10 @@
8
8
  * (`CREATE TEMP VIEW`, or a `CREATE TEMP TABLE` holding the columns' types,
9
9
  * defaults and generated expressions and the checks) inside a transaction
10
10
  * that is always rolled back, with a short `lock_timeout`, and read back with
11
- * the same printers the catalog read uses. Nothing is left behind: Postgres
11
+ * the same printers the catalog read uses. A trigger's `WHEN` and a policy's
12
+ * `USING` and `WITH CHECK`, which the server prints with the casts it
13
+ * resolved, are read back from a trigger or policy on a temporary copy of the
14
+ * table (`CREATE TEMP TABLE ... (LIKE table)`). Nothing is left behind: Postgres
12
15
  * DDL is transactional and a temporary object lives in the session's own
13
16
  * schema. A server that refuses (no TEMP privilege, a lock not granted in
14
17
  * time, an expression it rejects) leaves the rules' answer standing.
@@ -108,6 +111,27 @@ async function triggerWhen(client: PostgresClient, o: CanonicalPgObject, default
108
111
  });
109
112
  }
110
113
 
114
+ /** A policy's USING and WITH CHECK as the server prints them, canonical, from a policy on a temporary copy of its table. */
115
+ async function policyExpressions(client: PostgresClient, o: CanonicalPgObject, defaultSchema: string): Promise<{ using?: string; check?: string } | undefined> {
116
+ const using = o.fields.using as string | undefined;
117
+ const check = o.fields.check as string | undefined;
118
+ if (using === undefined && check === undefined) return undefined;
119
+ return rolledBack(client, defaultSchema, async () => {
120
+ await client.query(`CREATE TEMP TABLE chant_normalize (LIKE ${String(o.fields.table)})`);
121
+ await client.query(
122
+ [`CREATE POLICY chant_normalize ON pg_temp.chant_normalize FOR ${String(o.fields.command ?? "all").toUpperCase()}`, using !== undefined ? `USING (${using})` : "", check !== undefined ? `WITH CHECK (${check})` : ""]
123
+ .filter(Boolean)
124
+ .join(" "),
125
+ );
126
+ await emptyPath(client);
127
+ const [row] = await client.query<Row>(
128
+ `SELECT pg_catalog.pg_get_expr(p.polqual, p.polrelid, true) AS qual, pg_catalog.pg_get_expr(p.polwithcheck, p.polrelid, true) AS withcheck
129
+ FROM pg_catalog.pg_policy p WHERE p.polrelid = 'pg_temp.chant_normalize'::pg_catalog.regclass`,
130
+ );
131
+ return { ...(row?.qual ? { using: canonicalExpr(String(row.qual)) } : {}), ...(row?.withcheck ? { check: canonicalExpr(String(row.withcheck)) } : {}) };
132
+ });
133
+ }
134
+
111
135
  /**
112
136
  * The declared object with its expressions in the server's own printing,
113
137
  * when the server answers; the object unchanged otherwise. Only the
@@ -119,6 +143,10 @@ export async function serverNormalized<T extends CanonicalPgObject & { outputs?:
119
143
  // A declared column list names the outputs; otherwise the server's names do (`SELECT *` expanded).
120
144
  return q === undefined ? o : { ...o, fields: { ...o.fields, query: q.query }, ...(o.fields.columns === undefined ? { outputs: q.outputs } : {}) };
121
145
  }
146
+ if (o.kind === "policy") {
147
+ const e = await policyExpressions(client, o, defaultSchema);
148
+ return e === undefined ? o : { ...o, fields: { ...o.fields, ...e } };
149
+ }
122
150
  if (o.kind === "trigger") {
123
151
  const when = await triggerWhen(client, o, defaultSchema);
124
152
  return when === undefined ? o : { ...o, fields: { ...o.fields, when } };
package/src/postgres.ts CHANGED
@@ -18,6 +18,9 @@ export {
18
18
  func,
19
19
  procedure,
20
20
  trigger,
21
+ policy,
22
+ role,
23
+ grant,
21
24
  literal,
22
25
  SqlLiteral,
23
26
  SqlTemplateError,
@@ -39,6 +42,9 @@ export {
39
42
  type PostgresFunction,
40
43
  type PostgresProcedure,
41
44
  type PostgresTrigger,
45
+ type PostgresPolicy,
46
+ type PostgresRole,
47
+ type PostgresGrant,
42
48
  type ColumnDef,
43
49
  type KeyDef,
44
50
  type CheckDef,
@@ -57,6 +63,11 @@ export {
57
63
  type FunctionProps,
58
64
  type ProcedureProps,
59
65
  type TriggerProps,
66
+ type PolicyProps,
67
+ type RoleProps,
68
+ type GrantProps,
69
+ type DefaultPrivilegesProps,
70
+ type PrivilegeDef,
60
71
  type LineageEdge,
61
72
  } from "./postgres/entities";
62
73
 
package/src/serializer.ts CHANGED
@@ -12,7 +12,8 @@
12
12
  *
13
13
  * The order is the dependency order of the references: a view after the
14
14
  * tables it reads, a materialized view after its target, a table after its
15
- * database. Ties break by export name, so the output is the same however the
15
+ * database. An object read from a `.sql` file (`./sql-files.ts`) that a
16
+ * template names in its text counts as referenced too. Ties break by export name, so the output is the same however the
16
17
  * files were discovered. A reference cycle is an error naming the cycle.
17
18
  *
18
19
  * One build holds one dialect: the dialect is the document's, and a project
@@ -35,6 +36,7 @@ import { isClickHouseObject, type ClickHouseObject } from "./clickhouse/entities
35
36
  import { isPostgresObject, type PostgresObject } from "./postgres/entities";
36
37
  import { POSTGRES_LATEST_MAJOR } from "./spec/postgres-pin";
37
38
  import { applyOrder, lineageJson, referenceName, type LineageEdge } from "./core/references";
39
+ import { sqlFileReferences } from "./sql-files";
38
40
 
39
41
  export { applyOrder } from "./core/references";
40
42
 
@@ -44,7 +46,7 @@ export const CLICKHOUSE_DDL_FILE = "clickhouse.sql";
44
46
  /** The Postgres statements' file, beside the primary output. */
45
47
  export const POSTGRES_DDL_FILE = "postgres.sql";
46
48
 
47
- function objectJson(name: string, obj: ClickHouseObject, names: Map<Declarable, string>): Record<string, unknown> {
49
+ function objectJson(name: string, obj: ClickHouseObject, names: Map<Declarable, string>, extra: readonly string[] = []): Record<string, unknown> {
48
50
  const props = { ...(obj.props as Record<string, unknown>) };
49
51
  delete props.source;
50
52
  const ddl = props.ddl;
@@ -54,7 +56,7 @@ function objectJson(name: string, obj: ClickHouseObject, names: Map<Declarable,
54
56
  if (props.lineage) {
55
57
  props.lineage = lineageJson(props.lineage as LineageEdge[], names);
56
58
  }
57
- const dependsOn = [...new Set(obj.dependsOn.map((r) => referenceName(r, names)).filter((n) => n !== undefined))];
59
+ const dependsOn = [...new Set([...obj.dependsOn.map((r) => referenceName(r, names)).filter((n) => n !== undefined), ...extra])];
58
60
  return JSON.parse(
59
61
  JSON.stringify({ export: name, type: obj.entityType, sqlName: obj.sqlName, ...props, dependsOn, ddl }),
60
62
  ) as Record<string, unknown>;
@@ -68,12 +70,12 @@ function postgresJsonValue(v: unknown, names: Map<Declarable, string>): unknown
68
70
  return v;
69
71
  }
70
72
 
71
- function postgresObjectJson(name: string, obj: PostgresObject, names: Map<Declarable, string>): Record<string, unknown> {
73
+ function postgresObjectJson(name: string, obj: PostgresObject, names: Map<Declarable, string>, extra: readonly string[] = []): Record<string, unknown> {
72
74
  const props = { ...(obj.props as Record<string, unknown>) };
73
75
  delete props.source;
74
76
  const ddl = props.ddl;
75
77
  delete props.ddl;
76
- const dependsOn = [...new Set(obj.dependsOn.map((r) => referenceName(r, names)).filter((n) => n !== undefined))];
78
+ const dependsOn = [...new Set([...obj.dependsOn.map((r) => referenceName(r, names)).filter((n) => n !== undefined), ...extra])];
77
79
  return JSON.parse(
78
80
  JSON.stringify({ export: name, type: obj.entityType, sqlName: obj.sqlName, ...(postgresJsonValue(props, names) as object), dependsOn, ddl }),
79
81
  ) as Record<string, unknown>;
@@ -92,12 +94,14 @@ function serializePostgres(
92
94
  ): SerializerResult {
93
95
  const names = new Map<Declarable, string>();
94
96
  for (const [name, entity] of entities) names.set(entity, name);
95
- const order = applyOrder(objects, names);
97
+ // A template names an object read from a `.sql` file as text: that is an edge too.
98
+ const extra = sqlFileReferences("postgres", objects);
99
+ const order = applyOrder(objects, names, extra);
96
100
  const doc = {
97
101
  dialect: "postgres",
98
102
  postgresMajor: targetMajor(config),
99
103
  applyOrder: order,
100
- objects: order.map((n) => postgresObjectJson(n, objects.get(n)!, names)),
104
+ objects: order.map((n) => postgresObjectJson(n, objects.get(n)!, names, extra.get(n))),
101
105
  };
102
106
  const ddl = order.map((n) => `${(objects.get(n)!.props as { ddl: string }).ddl};`).join("\n\n");
103
107
  return {
@@ -130,11 +134,12 @@ export const sqlSerializer: Serializer = {
130
134
  const names = new Map<Declarable, string>();
131
135
  for (const [name, entity] of entities) names.set(entity, name);
132
136
 
133
- const order = applyOrder(objects, names);
137
+ const extra = sqlFileReferences("clickhouse", objects);
138
+ const order = applyOrder(objects, names, extra);
134
139
  const doc = {
135
140
  dialect: "clickhouse",
136
141
  applyOrder: order,
137
- objects: order.map((n) => objectJson(n, objects.get(n)!, names)),
142
+ objects: order.map((n) => objectJson(n, objects.get(n)!, names, extra.get(n))),
138
143
  };
139
144
  const ddl = order
140
145
  .map((n) => `${(objects.get(n)!.props as { ddl: string }).ddl};`)
package/src/skill-defs.ts CHANGED
@@ -54,7 +54,7 @@ export const sqlSkills = createSkillsLoader(import.meta.url, [
54
54
  file: "chant-sql-postgres.md",
55
55
  name: "chant-sql-postgres",
56
56
  description:
57
- "Declare Postgres schemas, tables, constraints, indexes, views, sequences, enum and domain types, extensions, functions, procedures and triggers as SQL-shaped tagged templates, with references, lineage and the SQLPG lint rules",
57
+ "Declare Postgres schemas, tables, constraints, indexes, views, sequences, enum and domain types, extensions, functions, procedures, triggers, policies, roles and grants as SQL-shaped tagged templates, with references, lineage and the SQLPG lint rules",
58
58
  triggers: [
59
59
  { type: "context" as const, value: "postgres" },
60
60
  { type: "context" as const, value: "postgres schema" },
@@ -40,6 +40,8 @@ Options besides `name`, `env`, `table`, `column`, `using`:
40
40
  - `gate` and `contractGate`: the two approvals, each with `gate` (name), `timeout` and `description`; `gate` also takes `approval` for quorum, roles and a policy, and a policy gets `verifiedRows` and `mismatched` as context;
41
41
  - `backfillTimeout` (default `6h`), `output` (default `dist/schema.json`), `path`, `build` (default true runs `chant build` first), `stack`, `ownershipEnv`.
42
42
 
43
+ Run as one step of a change approved elsewhere: `gates: "outer"` leaves out the switch gate, the contract gate and the Contract phase (verification still runs; the old column is kept until its retention date), and `onFailure: "keep"` leaves out onFailure, so a failed run's next run resumes the backfill from its receipts. Set them rather than editing the Op's phases.
44
+
43
45
  ## What it does
44
46
 
45
47
  | Phase | Step |
@@ -103,7 +105,7 @@ or set `replicationLag: false` to run without the check.
103
105
 
104
106
  ## When something goes wrong
105
107
 
106
- - A failed step (a value the `using` expression cannot convert, a verification mismatch, a refused declaration, an index that cannot be built) runs onFailure: the trigger, its function, the check, the carried indexes and constraints, the new column and the receipts table are dropped, and the plan shows the original change again. The old column was never touched. Fix the declaration or the data and run again. A failed gate or an aborted run does not run onFailure.
108
+ - A failed step (a value the `using` expression cannot convert, a verification mismatch, a refused declaration, an index that cannot be built) runs onFailure, unless the Op sets `onFailure: "keep"`: the trigger, its function, the check, the carried indexes and constraints, the new column and the receipts table are dropped, and the plan shows the original change again. The old column was never touched. Fix the declaration or the data and run again. A failed gate or an aborted run does not run onFailure.
107
109
  - A lock timeout means another session holds a lock the step needs (SQLSTATE 55P03). The step retries a few times, then fails. Run again when the blocker has finished.
108
110
  - The working objects (new column, trigger, function, check, receipts table) carry chant's comment marker with a `migration=` key, and plans, imports and prunes leave them out, so an `ApplyOp` for the rest of the schema can run beside a migration. An object under one of the working names that is not this migration's stops the run; rename or drop it by hand.
109
111
 
@@ -83,9 +83,9 @@ Every rule has an id in the SQLPG2xx range, names its class and cites the Postgr
83
83
  | column type to a binary-coercible type (longer varchar, text, wider numeric) | SQLPG206 | metadata | indexes on the column may still be rebuilt |
84
84
  | column type with a rewrite (integer to bigint, a shorter varchar) | SQLPG207 | rewrite | |
85
85
  | column type across kinds (text to integer) | SQLPG208 | expand | needs USING and breaks readers |
86
- | set NOT NULL | SQLPG210 | rewrite | a valid `CHECK (col IS NOT NULL)` already in place makes it metadata |
87
- | add a CHECK | SQLPG218 | rewrite | declare it `NOT VALID`, then validate |
88
- | add a foreign key | SQLPG219 | validate | `SHARE ROW EXCLUSIVE` on both tables for the scan; `NOT VALID` first avoids it |
86
+ | set NOT NULL | SQLPG210 | validate | made as a `NOT VALID` check, `VALIDATE`, `SET NOT NULL`, the check dropped; a valid `CHECK (col IS NOT NULL)` already in place makes it metadata |
87
+ | add a CHECK | SQLPG218 | validate | made as `ADD CONSTRAINT ... NOT VALID`, then `VALIDATE` in its own transaction; writes go on during the scan |
88
+ | add a foreign key | SQLPG219 | validate | made as `ADD CONSTRAINT ... NOT VALID`, then `VALIDATE` in its own transaction; writes go on during the scan |
89
89
  | add a constraint `NOT VALID` | SQLPG217 | metadata | checks new rows only |
90
90
  | validate a constraint | SQLPG220 | validate | `SHARE UPDATE EXCLUSIVE`, so reads and writes go on |
91
91
  | add a primary key or unique constraint | SQLPG221 | concurrently | build the unique index CONCURRENTLY, then `ADD CONSTRAINT ... USING INDEX` |
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  skill: chant-sql-postgres
3
- description: Declare Postgres schemas, tables, constraints, indexes, views, sequences, enum and domain types, extensions, functions, procedures and triggers as SQL-shaped tagged templates, with references, lineage and the SQLPG lint rules
3
+ description: Declare Postgres schemas, tables, constraints, indexes, views, sequences, enum and domain types, extensions, functions, procedures, triggers, policies, roles and grants as SQL-shaped tagged templates, with references, lineage and the SQLPG lint rules
4
4
  user-invocable: true
5
5
  ---
6
6
  # Declaring a Postgres schema with chant
@@ -40,6 +40,9 @@ The export name (`users`) is the object's identity in chant. The name in the SQL
40
40
  | `func` | `CREATE FUNCTION` | `Postgres::Function` |
41
41
  | `procedure` | `CREATE PROCEDURE` | `Postgres::Procedure` |
42
42
  | `trigger` | `CREATE TRIGGER` | `Postgres::Trigger` |
43
+ | `policy` | `CREATE POLICY` | `Postgres::Policy` |
44
+ | `role` | `CREATE ROLE` (no password, no memberships) | `Postgres::Role` |
45
+ | `grant` | `GRANT`, `REVOKE`, `ALTER DEFAULT PRIVILEGES` | `Postgres::Grant`, `Postgres::DefaultPrivileges` |
43
46
 
44
47
  `table` and `view` imported from the package root are ClickHouse's. Import every tag from `@intentius/chant-lexicon-sql/postgres`.
45
48
 
@@ -140,6 +143,10 @@ export const ordersTouch = trigger`CREATE TRIGGER orders_touch BEFORE UPDATE ON
140
143
 
141
144
  The tag is `func` (`function` is a JavaScript key word). Write the body as a string, `AS $$ ... $$`: it is kept and compared verbatim. `${users}` inside the body renders as `app.users` and makes the routine depend on the table. A SQL-standard body (`RETURN ...`, `BEGIN ATOMIC`) and `SET ... FROM CURRENT` are SQLPG001 errors. Overloads are separate objects, identified by their input parameter types. A trigger's name is unqualified; it lives on its table.
142
145
 
146
+ ## Access
147
+
148
+ A table's template may end with `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`. `policy`, `role` and `grant` declare policies, the roles the schema needs (never a password or a membership: those are the environment's), privileges on schemas, tables, columns, sequences and routines, and default privileges. They are planned and applied only where `sql.profiles.<env>.access` is true; there, the declared privileges are the whole of them, and one granted by hand is revoked. A role the environment provisions is named as text (`TO support`), never declared. `GRANT ... ON ALL TABLES IN SCHEMA` is refused: grant per table and declare default privileges.
149
+
143
150
  ## Comments
144
151
 
145
152
  Postgres has no inline comment clause. Follow the `CREATE` with `COMMENT ON` for the object, its columns and its named constraints, separated by `;`:
@@ -178,4 +185,4 @@ chant build src --lexicon sql -o dist/schema.json
178
185
 
179
186
  ## Not covered
180
187
 
181
- `ALTER`, `CREATE POLICY`, `GRANT`, rules, aggregates, event triggers and the composite and range forms of `CREATE TYPE` are not declared with the tags. Planning a schema change (`chant sql diff`, `chant sql plan`, the lock classes) is the `chant-sql-postgres-plan` skill. Apply and the expand-and-contract migration Op have their own skills, which arrive with the code they describe.
188
+ `ALTER` (other than a table's row-level security), role memberships, rules, aggregates, event triggers and the composite and range forms of `CREATE TYPE` are not declared with the tags. Planning a schema change (`chant sql diff`, `chant sql plan`, the lock classes) is the `chant-sql-postgres-plan` skill. Apply and the expand-and-contract migration Op have their own skills, which arrive with the code they describe.
@@ -26,7 +26,7 @@ export const { op } = ClickHouseRebuildOp({
26
26
  });
27
27
  ```
28
28
 
29
- Options: `name`, `env`, `table`, `dualWrite` (required); `output` (default `dist/schema.json`), `path`, `build` (default true runs `chant build` first), `retain` (default `7d`), `gate` (the swap gate: `gate`, `timeout`, `description`, `approval`), `dropGate`, `writesGate` (app mode), `backfillTimeout` (default `6h`), `mutationTimeout` (default `10m`), `replicaTimeout` (default `2m`, Replicated databases only), `stack`, `ownershipEnv`.
29
+ Options: `name`, `env`, `table`, `dualWrite` (required); `output` (default `dist/schema.json`), `path`, `build` (default true runs `chant build` first), `retain` (default `7d`), `gate` (the swap gate: `gate`, `timeout`, `description`, `approval`), `dropGate`, `writesGate` (app mode), `backfillTimeout` (default `6h`), `mutationTimeout` (default `10m`), `replicaTimeout` (default `2m`, Replicated databases only), `cutoverTimeout` (default `10m`, materialized-view mode only), `stack`, `ownershipEnv`.
30
30
 
31
31
  On a `cluster:<name>` topology with more than one shard, the backfill and the verification work per shard through the profile's server, with a receipt per shard and partition; each shard needs its own `{shard}` macro.
32
32
 
@@ -34,7 +34,7 @@ Run as one step of a change approved elsewhere: `gates: "outer"` leaves out the
34
34
 
35
35
  Pick the dual-write mode:
36
36
 
37
- - `{ mode: "materialized-view", cutoverColumn: "ts", cutoverDelay?: "1m" }` when the table has a time column rows arrive in order of. Writes keep flowing: a materialized view sends rows at or after the cut-over to the new table, the backfill copies the rows before it.
37
+ - `{ mode: "materialized-view", cutoverColumn: "ts", cutoverDelay?: "5s" }` when the table has a time column rows arrive in order of. Writes keep flowing: a materialized view sends rows at or after the cut-over to the new table, the backfill copies the rows before it. The backfill starts once the server's clock has passed the cut-over and every INSERT into the old table begun before it has finished, asynchronous inserts still in their buffer included; `cutoverTimeout` bounds the second wait. Set `cutoverDelay` longer than the batches of a writer that holds rows before inserting them.
38
38
  - `{ mode: "app" }` otherwise. A gate, `<name>-writes-stopped`, waits for someone to confirm the application stopped writing to the table; writes resume after the swap.
39
39
 
40
40
  ## Run it
@@ -57,6 +57,7 @@ The swap gate is bound to a digest of the plan and the verification (row counts
57
57
  - A run stopped during the backfill (Ctrl-C, a killed job) is not a failure. The next run resumes from the receipts in `chant_receipts.receipts` on the same server (in `<db>.__chant_receipts` on a Replicated database), and a partition whose copy was cut off before its receipt is cleared and copied again, so no row is copied twice.
58
58
  - A failed step (a failed copy after its retries, a verification mismatch, a refused declaration) runs onFailure: the new table `<t>__chant_new` and the dual-write view `<t>__chant_dual` are dropped, and the next run starts from a new table. With `onFailure: "keep"` they stay, and the next run resumes; drop them by hand to start over. The original table is untouched until the swap.
59
59
  - A verification mismatch in materialized-view mode usually means rows arrived with a time before the cut-over later than `cutoverDelay`. Raise the delay and run again.
60
+ - A backfill that stops naming writes "begun before the cut-over" waited `cutoverTimeout` for INSERTs into the old table that were still running. Let them finish, or stop them, and run again.
60
61
  - The Op refuses a database that is neither Atomic nor Replicated (`EXCHANGE TABLES` needs one of them), a table whose change is not a rebuild (use the applier), and an object already under one of its working names that is not its own. In a Replicated database it also refuses a table, or a declaration, whose engine is not `Replicated*MergeTree`: a plain MergeTree keeps different rows on each replica and one replica's copy would lose the others'.
61
62
 
62
63
  ## On a Replicated database
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  skill: chant-sql
3
- description: Declare ClickHouse databases, tables, views and materialized views as SQL-shaped tagged templates, with references, lineage and the checks that run on them
3
+ description: Declare ClickHouse databases, tables, views, materialized views, dictionaries and functions as SQL-shaped tagged templates, with references, lineage and the checks that run on them
4
4
  user-invocable: true
5
5
  ---
6
6
  # Declaring a ClickHouse schema with chant
@@ -33,6 +33,12 @@ export const byKind = view`
33
33
 
34
34
  The export name (`events`) is the object's identity in chant. The name in the SQL (`analytics.events`) is its name in the database. A materialized view is a `view` template holding `CREATE MATERIALIZED VIEW`.
35
35
 
36
+ A dictionary is a `dictionary` template holding `CREATE DICTIONARY`, with its attributes, `PRIMARY KEY`, `SOURCE`, `LAYOUT` and `LIFETIME`. Its attributes are columns (`${ratesDict.columns.rate}`). Any change but its comment is SQLCH245, and the applier replaces it with `CREATE OR REPLACE DICTIONARY`. Keep passwords out of `SOURCE`: name a server-side named collection instead.
37
+
38
+ A SQL user-defined function is a `func` template holding `CREATE FUNCTION name AS (x) -> expr`, with no database in its name. A changed expression is SQLCH260 (`CREATE OR REPLACE FUNCTION`). It carries no ownership marker, so chant never drops one.
39
+
40
+ Users, roles, row policies and grants are `user`, `role`, `policy` and `grant` templates. Never write a password: `IDENTIFIED BY` is refused, a user declared without `IDENTIFIED` is created by the environment with its password, and chant manages the rest of it. Grants are compared per grantee: every `grant` naming a grantee adds up to its complete list, and anything else it holds is revoked (SQLCH274). None of these is ever dropped by chant.
41
+
36
42
  ## Interpolations
37
43
 
38
44
  | Value | Means | Renders as |
@@ -0,0 +1,188 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
2
+ import { mkdir, realpath, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { build } from "@intentius/chant/build";
6
+ import { readSqlFile, sqlFileDeclarations, SqlFileError } from "./sql-files";
7
+ import { sqlPlugin } from "./plugin";
8
+ import { sqlSerializer } from "./serializer";
9
+ import { guessDialect, SqlFileParser } from "./import-parser";
10
+
11
+ const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
12
+
13
+ const PG_DDL = `BEGIN;
14
+ CREATE TYPE app.status AS ENUM ('placed', 'paid');
15
+ CREATE TABLE app.orders (
16
+ id bigint PRIMARY KEY,
17
+ user_id bigint NOT NULL,
18
+ status app.status NOT NULL
19
+ );
20
+ CREATE TABLE app.users (id bigint PRIMARY KEY, email text NOT NULL);
21
+ ALTER TABLE app.orders ADD CONSTRAINT orders_user_fk FOREIGN KEY (user_id) REFERENCES app.users (id);
22
+ CREATE INDEX orders_user_idx ON app.orders (user_id);
23
+ COMMENT ON TABLE app.users IS 'People';
24
+ COMMENT ON COLUMN app.users.email IS 'Login';
25
+ ALTER TABLE app.users ENABLE ROW LEVEL SECURITY;
26
+ COMMIT;
27
+ `;
28
+
29
+ describe("reading a file of Postgres DDL", () => {
30
+ test("each CREATE is an object, with its comments, row security and added constraints folded in", () => {
31
+ const objects = readSqlFile("postgres", PG_DDL, { origin: "schema.sql" });
32
+ expect(objects.map((o) => [o.export, o.type, o.name])).toEqual([
33
+ ["status", "Postgres::Enum", "app.status"],
34
+ ["orders", "Postgres::Table", "app.orders"],
35
+ ["users", "Postgres::Table", "app.users"],
36
+ ["ordersUserIdx", "Postgres::Index", "app.orders_user_idx"],
37
+ ]);
38
+ expect(objects[1]!.ddl).toContain("CONSTRAINT orders_user_fk FOREIGN KEY (user_id) REFERENCES app.users (id)\n)");
39
+ expect(objects[2]!.ddl).toBe(
40
+ "CREATE TABLE app.users (id bigint PRIMARY KEY, email text NOT NULL);\nCOMMENT ON TABLE app.users IS 'People';\nCOMMENT ON COLUMN app.users.email IS 'Login';\nALTER TABLE app.users ENABLE ROW LEVEL SECURITY",
41
+ );
42
+ });
43
+
44
+ test("a statement that declares nothing or does not parse is an error naming it, and every one is named", () => {
45
+ const read = () => readSqlFile("postgres", "CREATE TABLE a (id int);\nINSERT INTO a VALUES (1);\nCREATE TABLE b (id int,);\nCOMMENT ON TABLE c IS 'x';", { origin: "db/schema.sql" });
46
+ expect(read).toThrow(SqlFileError);
47
+ expect(read).toThrow(/^db\/schema\.sql: 3 statements chant cannot read as declarations/);
48
+ expect(read).toThrow(/not a statement that declares an object: INSERT INTO a VALUES \(1\)/);
49
+ expect(read).toThrow(/does not parse \(.*\): CREATE TABLE b \(id int,\)/);
50
+ expect(read).toThrow(/COMMENT ON TABLE for an object the file does not create: COMMENT ON TABLE c IS 'x'/);
51
+ });
52
+
53
+ test("schema qualifies the names the DDL leaves unqualified", () => {
54
+ const { content } = sqlFileDeclarations("postgres", "CREATE TYPE s AS ENUM ('a');\nCREATE TABLE u (id int PRIMARY KEY, k s);\nCREATE TABLE o (u int REFERENCES u (id));", { origin: "x", schema: "app" });
55
+ expect(content).toContain("CREATE TABLE app.u (id int PRIMARY KEY, k ${s})");
56
+ expect(content).toContain("CREATE TABLE app.o (u int REFERENCES ${u} (id))");
57
+ });
58
+ });
59
+
60
+ describe("chant import of a .sql file", () => {
61
+ test("the dialect is read off the statements, and Postgres DDL imports as Postgres declarations", () => {
62
+ expect(guessDialect(PG_DDL)).toBe("postgres");
63
+ expect(guessDialect("CREATE TABLE a.t (x UInt8) ENGINE = Log")).toBe("clickhouse");
64
+ expect(guessDialect("CREATE VIEW a.v AS SELECT 1")).toBe("clickhouse");
65
+ expect(guessDialect("CREATE FUNCTION f AS (x) -> x + 1")).toBe("clickhouse");
66
+ expect(guessDialect("CREATE FUNCTION app.f() RETURNS int LANGUAGE sql AS $$ SELECT 1 $$")).toBe("postgres");
67
+ expect(new SqlFileParser().parse(PG_DDL).resources.map((r) => r.type)).toEqual(["Postgres::Enum", "Postgres::Table", "Postgres::Table", "Postgres::Index"]);
68
+ });
69
+
70
+ test("a statement it cannot read stops the import, named", () => {
71
+ expect(() => new SqlFileParser("clickhouse").parse("CREATE DATABASE a;\nINSERT INTO a.t VALUES (1)")).toThrow(/not a CREATE statement: INSERT INTO a\.t VALUES \(1\)/);
72
+ });
73
+ });
74
+
75
+ /** A project directory under the repository, so its imports resolve. */
76
+ async function project(name: string, files: Record<string, string>): Promise<string> {
77
+ const path = join(repoRoot, ".cache", `sql-3646-${name}-${process.pid}`);
78
+ await rm(path, { recursive: true, force: true });
79
+ await mkdir(join(path, "src"), { recursive: true });
80
+ const dir = await realpath(path);
81
+ for (const [file, content] of Object.entries(files)) await writeFile(join(dir, "src", file), content);
82
+ return dir;
83
+ }
84
+
85
+ /** A build document with each DDL's whitespace runs collapsed. */
86
+ const squeeze = (doc: unknown): unknown => JSON.parse(JSON.stringify(doc, (k, v: unknown) => (k === "ddl" && typeof v === "string" ? v.replace(/\s+/g, " ") : v)));
87
+
88
+ async function buildProject(dir: string, config: Record<string, unknown> = {}) {
89
+ const result = await build(join(dir, "src"), [sqlSerializer], undefined, {
90
+ lexicons: ["sql"],
91
+ intrinsics: sqlPlugin.intrinsics?.() ?? [],
92
+ buildRoots: [(ctx) => sqlPlugin.buildRoots!({ projectRoot: dir, config, entities: ctx.entities, ...(ctx.sourceDir ? { sourceDir: ctx.sourceDir } : {}) })],
93
+ });
94
+ const out = result.outputs.get("sql");
95
+ const primary = typeof out === "string" ? out : out?.primary;
96
+ return { errors: result.errors.map((e) => e.message), doc: primary ? (JSON.parse(primary) as { applyOrder: string[]; objects: Array<Record<string, unknown>> }) : undefined };
97
+ }
98
+
99
+ const CH_DDL = `CREATE DATABASE analytics;
100
+ CREATE TABLE analytics.events (ts DateTime, kind String) ENGINE = MergeTree ORDER BY ts;
101
+ CREATE TABLE analytics.counts (kind String, n UInt64) ENGINE = SummingMergeTree ORDER BY kind;
102
+ CREATE MATERIALIZED VIEW analytics.counts_mv TO analytics.counts AS SELECT kind, count() AS n FROM analytics.events GROUP BY kind;
103
+ CREATE DICTIONARY analytics.kinds (kind String, label String DEFAULT '') PRIMARY KEY kind SOURCE(clickhouse(table 'counts' db 'analytics')) LAYOUT(complex_key_hashed()) LIFETIME(300);
104
+ CREATE FUNCTION kind_label AS (k) -> upper(k);
105
+ `;
106
+
107
+ describe("chant build reads the .sql files in the source directory", () => {
108
+ const dirs: string[] = [];
109
+ afterAll(async () => {
110
+ for (const d of dirs) await rm(d, { recursive: true, force: true });
111
+ });
112
+
113
+ for (const [dialect, ddl] of [
114
+ ["postgres", PG_DDL.replace("CREATE TYPE", "CREATE SCHEMA app;\nCREATE TYPE")],
115
+ ["clickhouse", CH_DDL],
116
+ ] as const) {
117
+ test(`${dialect}: the same build as the declarations chant import writes for the file`, async () => {
118
+ const fromSql = await project(`${dialect}-sql`, { "schema.sql": ddl });
119
+ const fromTs = await project(`${dialect}-ts`, { "schema.ts": sqlFileDeclarations(dialect, ddl, { origin: "schema.sql" }).content });
120
+ dirs.push(fromSql, fromTs);
121
+ const a = await buildProject(fromSql, { sql: { dialect } });
122
+ const b = await buildProject(fromTs, { sql: { dialect } });
123
+ expect(a.errors).toEqual([]);
124
+ expect(b.errors).toEqual([]);
125
+ // The import indents a template's lines under its declaration; the statements are otherwise the same.
126
+ expect(squeeze(a.doc)).toEqual(squeeze(b.doc));
127
+ expect(a.doc!.objects.length).toBeGreaterThan(2);
128
+ });
129
+ }
130
+
131
+ let mixed: string;
132
+ beforeAll(async () => {
133
+ mixed = await project("mixed", {
134
+ "app.ts": `import { schema, view } from "@intentius/chant-lexicon-sql/postgres";
135
+ export const app = schema\`CREATE SCHEMA app\`;
136
+ export const activeUsers = view\`CREATE VIEW app.active_users AS SELECT id FROM app.users\`;
137
+ `,
138
+ "users.sql": "CREATE TABLE app.users (id bigint PRIMARY KEY);\nCREATE TABLE app.notes (user_id bigint REFERENCES app.users (id));\n",
139
+ });
140
+ dirs.push(mixed);
141
+ });
142
+
143
+ test("a file's object and a template reference each other, and the order comes from the references", async () => {
144
+ const { errors, doc } = await buildProject(mixed);
145
+ expect(errors).toEqual([]);
146
+ const objects = new Map(doc!.objects.map((o) => [o.export as string, o]));
147
+ // The file's table names the template's schema; the template's view names the file's table as text.
148
+ expect(objects.get("users")!.dependsOn).toEqual(["app"]);
149
+ expect(objects.get("notes")!.dependsOn).toEqual(["app", "users"]);
150
+ expect(objects.get("activeUsers")!.dependsOn).toEqual(["users"]);
151
+ expect(doc!.applyOrder).toEqual(["app", "users", "activeUsers", "notes"]);
152
+ });
153
+
154
+ test("a statement the build cannot read is a build error naming the file and the statement", async () => {
155
+ const dir = await project("bad", { "app.ts": `import { schema } from "@intentius/chant-lexicon-sql/postgres";\nexport const app = schema\`CREATE SCHEMA app\`;\n`, "bad.sql": "CREATE TABLE app.t (id int);\nDROP TABLE app.old;\n" });
156
+ dirs.push(dir);
157
+ const { errors } = await buildProject(dir);
158
+ expect(errors.join("\n")).toMatch(/src\/bad\.sql: a statement chant cannot read as declarations:\n {2}- not a statement that declares an object: DROP TABLE app\.old/);
159
+ });
160
+
161
+ test("an object declared in a file and a template both is an error naming both", async () => {
162
+ const dir = await project("twice", { "app.ts": `import { table } from "@intentius/chant-lexicon-sql/postgres";\nexport const accounts = table\`CREATE TABLE app.users (id int)\`;\n`, "users.sql": "CREATE TABLE app.users (id int);\n" });
163
+ dirs.push(dir);
164
+ const { errors } = await buildProject(dir);
165
+ expect(errors.join("\n")).toMatch(/app\.users is declared twice, here and as accounts/);
166
+ });
167
+
168
+ test("a file marked chant-discovery-skip is not read", async () => {
169
+ const dir = await project("skip", { "app.ts": `import { schema } from "@intentius/chant-lexicon-sql/postgres";\nexport const app = schema\`CREATE SCHEMA app\`;\n`, "seed.sql": "-- chant-discovery-skip\nINSERT INTO app.t VALUES (1);\n" });
170
+ dirs.push(dir);
171
+ const { errors, doc } = await buildProject(dir);
172
+ expect(errors).toEqual([]);
173
+ expect(doc!.applyOrder).toEqual(["app"]);
174
+ });
175
+ });
176
+
177
+ describe("references between a file's objects", () => {
178
+ test("a bare name where SQL names an object is a reference, and a cycle is an error", async () => {
179
+ const { sqlFileEntities } = await import("./sql-files");
180
+ const entities = sqlFileEntities("postgres", [
181
+ { origin: "a.sql", ddl: "CREATE TABLE orders (user_id int REFERENCES users (id));\nCREATE TABLE users (id int PRIMARY KEY);\nCREATE VIEW recent AS SELECT * FROM orders JOIN users ON true;" },
182
+ ]);
183
+ const deps = (n: string) => ((entities.get(n) as unknown as { dependsOn: Array<{ sqlName: string }> }).dependsOn).map((d) => d.sqlName);
184
+ expect(deps("orders")).toEqual(["users"]);
185
+ expect(deps("recent")).toEqual(["orders", "users"]);
186
+ expect(() => sqlFileEntities("postgres", [{ origin: "b.sql", ddl: "CREATE TABLE a (b int REFERENCES b (id), id int PRIMARY KEY);\nCREATE TABLE b (a int REFERENCES a (id), id int PRIMARY KEY);" }])).toThrow(/reference each other/);
187
+ });
188
+ });