@intentius/chant-lexicon-sql 0.124.30 → 0.124.32
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/change-set.js +250 -0
- package/dist/clickhouse/access.js +722 -0
- package/dist/clickhouse/apply/apply.js +243 -0
- package/dist/clickhouse/apply/mutations.js +66 -0
- package/dist/clickhouse/apply/statements.js +545 -0
- package/dist/clickhouse/catalog-types.js +7 -0
- package/dist/clickhouse/container.js +152 -0
- package/dist/clickhouse/engine-syntax.js +159 -0
- package/dist/clickhouse/entities.js +593 -0
- package/dist/clickhouse/expressions.js +107 -0
- package/dist/clickhouse/http.js +55 -0
- package/dist/clickhouse/import/generator.js +192 -0
- package/dist/clickhouse/import/inlined.js +207 -0
- package/dist/clickhouse/import/ir.js +160 -0
- package/dist/clickhouse/import/live-export.js +131 -0
- package/dist/clickhouse/import/parser.js +17 -0
- package/dist/clickhouse/live/bind.js +116 -0
- package/dist/clickhouse/live/catalog.js +223 -0
- package/dist/clickhouse/live/describe-resources.js +91 -0
- package/dist/clickhouse/live/undeclarable.js +59 -0
- package/dist/clickhouse/overlays/codecs.js +57 -0
- package/dist/clickhouse/overlays/engines.js +101 -0
- package/dist/clickhouse/overlays/kinds.js +1 -0
- package/dist/clickhouse/overlays/projections.js +26 -0
- package/dist/clickhouse/overlays/skip-indexes.js +40 -0
- package/dist/clickhouse/overlays/ttl.js +34 -0
- package/dist/clickhouse/overlays/types.js +59 -0
- package/dist/clickhouse/ownership.js +83 -0
- package/dist/clickhouse/parser.js +674 -0
- package/dist/clickhouse/plan/commands.js +159 -0
- package/dist/clickhouse/plan/deep.js +345 -0
- package/dist/clickhouse/plan/diff.js +312 -0
- package/dist/clickhouse/plan/disruption.js +73 -0
- package/dist/clickhouse/plan/normalize.js +545 -0
- package/dist/clickhouse/plan/rebuild-handoff.js +57 -0
- package/dist/clickhouse/plan/report.js +26 -0
- package/dist/clickhouse/plan/rules.js +72 -0
- package/dist/clickhouse/plan/schema.js +67 -0
- package/dist/clickhouse/plan/server-format.js +35 -0
- package/dist/clickhouse/rebuild/backfill.js +265 -0
- package/dist/clickhouse/rebuild/engines.js +146 -0
- package/dist/clickhouse/rebuild/observe.js +260 -0
- package/dist/clickhouse/rebuild/op.js +152 -0
- package/dist/clickhouse/rebuild/partitions.js +65 -0
- package/dist/clickhouse/rebuild/receipts.js +120 -0
- package/dist/clickhouse/rebuild/replicas.js +121 -0
- package/dist/clickhouse/rebuild/shards.js +76 -0
- package/dist/clickhouse/rebuild/steps.js +383 -0
- package/dist/clickhouse/rebuild/verify.js +279 -0
- package/dist/clickhouse/row-types.js +151 -0
- package/dist/clickhouse/testing/fake-server.js +47 -0
- package/dist/clickhouse/testing/roundtrip-schema.js +25 -0
- package/dist/clickhouse/testing/writable-server.js +123 -0
- package/dist/clickhouse/tokens.js +23 -0
- package/dist/clickhouse/topology.js +463 -0
- package/dist/clickhouse/type-syntax.js +73 -0
- package/dist/clickhouse.js +25 -0
- package/dist/codegen/classifier-page.js +93 -0
- package/dist/codegen/clickhouse-module.js +249 -0
- package/dist/codegen/coverage-page.js +70 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +222 -0
- package/dist/codegen/generate-cli.js +13 -0
- package/dist/codegen/generate.js +97 -0
- package/dist/codegen/package.js +28 -0
- package/dist/codegen/postgres-classifier-page.js +95 -0
- package/dist/codegen/postgres-module.js +418 -0
- package/dist/codegen/registry.js +52 -0
- package/dist/composites/audit-log-table.js +35 -0
- package/dist/composites/catalog.js +524 -0
- package/dist/composites/cdc-mirror.js +32 -0
- package/dist/composites/clickhouse.js +6 -0
- package/dist/composites/events-table.js +26 -0
- package/dist/composites/index.js +8 -0
- package/dist/composites/join-table.js +25 -0
- package/dist/composites/postgres.js +6 -0
- package/dist/composites/refreshed-view.js +24 -0
- package/dist/composites/replacing-table.js +28 -0
- package/dist/composites/rollup-view.js +29 -0
- package/dist/composites/sharded-table.js +27 -0
- package/dist/composites/soft-delete-table.js +34 -0
- package/dist/composites/tenant-table.js +26 -0
- package/dist/composites/typed.js +8 -0
- package/dist/config.js +206 -0
- package/dist/core/apply.js +93 -0
- package/dist/core/classifier.js +115 -0
- package/dist/core/container.js +69 -0
- package/dist/core/cursor.js +261 -0
- package/dist/core/diff.js +48 -0
- package/dist/core/entity.js +58 -0
- package/dist/core/find-templates.js +118 -0
- package/dist/core/foreign-tables.js +27 -0
- package/dist/core/handoff.js +32 -0
- package/dist/core/index.js +64 -0
- package/dist/core/interpolation.js +191 -0
- package/dist/core/normalize.js +43 -0
- package/dist/core/ownership.js +119 -0
- package/dist/core/receipts.js +22 -0
- package/dist/core/references.js +61 -0
- package/dist/core/statements.js +42 -0
- package/dist/core/template.js +79 -0
- package/dist/core/testing/http-stub.js +21 -0
- package/dist/core/tokens.js +202 -0
- package/dist/core/typed.js +40 -0
- package/dist/core/undeclarable.js +46 -0
- package/dist/coverage.js +161 -0
- package/dist/detect.js +32 -0
- package/dist/dialects.js +18 -0
- package/dist/files/build-root.js +72 -0
- package/dist/files/clickhouse.js +138 -0
- package/dist/files/common.js +58 -0
- package/dist/files/postgres.js +389 -0
- package/dist/generated/clickhouse-types.js +5 -0
- package/dist/generated/clickhouse.js +4361 -0
- package/dist/generated/postgres-types.js +227 -0
- package/dist/generated/postgres.js +3667 -0
- package/dist/import-generator.js +14 -0
- package/dist/import-parser.js +71 -0
- package/dist/index.js +24 -0
- package/dist/init-templates.js +290 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +31 -0
- package/dist/lint/audit-lineage.js +22 -0
- package/dist/lint/post-synth/audit-entries.js +34 -0
- package/dist/lint/post-synth/clickhouse-helpers.js +79 -0
- package/dist/lint/post-synth/clickhouse-type-check.js +162 -0
- package/dist/lint/post-synth/index.js +112 -0
- package/dist/lint/post-synth/postgres-audit-entries.js +35 -0
- package/dist/lint/post-synth/postgres-helpers.js +65 -0
- package/dist/lint/post-synth/postgres-names.js +507 -0
- package/dist/lint/post-synth/sql-helpers.js +23 -0
- package/dist/lint/post-synth/sql101.js +59 -0
- package/dist/lint/post-synth/sql102.js +34 -0
- package/dist/lint/post-synth/sqlch101.js +34 -0
- package/dist/lint/post-synth/sqlch102.js +23 -0
- package/dist/lint/post-synth/sqlch103.js +45 -0
- package/dist/lint/post-synth/sqlch104.js +41 -0
- package/dist/lint/post-synth/sqlch105.js +36 -0
- package/dist/lint/post-synth/sqlch106.js +27 -0
- package/dist/lint/post-synth/sqlch107.js +39 -0
- package/dist/lint/post-synth/sqlch108.js +28 -0
- package/dist/lint/post-synth/sqlch109.js +21 -0
- package/dist/lint/post-synth/sqlch110.js +31 -0
- package/dist/lint/post-synth/sqlch111.js +41 -0
- package/dist/lint/post-synth/sqlch112.js +28 -0
- package/dist/lint/post-synth/sqlch113.js +19 -0
- package/dist/lint/post-synth/sqlch114.js +26 -0
- package/dist/lint/post-synth/sqlch115.js +30 -0
- package/dist/lint/post-synth/sqlch116.js +22 -0
- package/dist/lint/post-synth/sqlch117.js +26 -0
- package/dist/lint/post-synth/sqlch118.js +27 -0
- package/dist/lint/post-synth/sqlch119.js +32 -0
- package/dist/lint/post-synth/sqlch120.js +20 -0
- package/dist/lint/post-synth/sqlch121.js +11 -0
- package/dist/lint/post-synth/sqlch122.js +11 -0
- package/dist/lint/post-synth/sqlch123.js +10 -0
- package/dist/lint/post-synth/sqlch124.js +21 -0
- package/dist/lint/post-synth/sqlch125.js +55 -0
- package/dist/lint/post-synth/sqlch126.js +62 -0
- package/dist/lint/post-synth/sqlch127.js +132 -0
- package/dist/lint/post-synth/sqlpg101.js +25 -0
- package/dist/lint/post-synth/sqlpg102.js +35 -0
- package/dist/lint/post-synth/sqlpg103.js +23 -0
- package/dist/lint/post-synth/sqlpg104.js +23 -0
- package/dist/lint/post-synth/sqlpg105.js +22 -0
- package/dist/lint/post-synth/sqlpg106.js +22 -0
- package/dist/lint/post-synth/sqlpg107.js +22 -0
- package/dist/lint/post-synth/sqlpg108.js +28 -0
- package/dist/lint/post-synth/sqlpg109.js +39 -0
- package/dist/lint/post-synth/sqlpg110.js +38 -0
- package/dist/lint/post-synth/sqlpg111.js +23 -0
- package/dist/lint/post-synth/sqlpg112.js +25 -0
- package/dist/lint/post-synth/sqlpg113.js +29 -0
- package/dist/lint/post-synth/sqlpg114.js +39 -0
- package/dist/lint/post-synth/sqlpg115.js +48 -0
- package/dist/lint/post-synth/sqlpg116.js +26 -0
- package/dist/lint/post-synth/sqlpg117.js +24 -0
- package/dist/lint/post-synth/sqlpg118.js +22 -0
- package/dist/lint/post-synth/sqlpg119.js +41 -0
- package/dist/lint/post-synth/sqlpg120.js +83 -0
- package/dist/lint/post-synth/sqlpg121.js +33 -0
- package/dist/lint/post-synth/sqlpg122.js +27 -0
- package/dist/lint/post-synth/sqlpg123.js +69 -0
- package/dist/lint/post-synth/sqlpg124.js +50 -0
- package/dist/lint/post-synth/sqlpg125.js +41 -0
- package/dist/lint/post-synth/sqlpg126.js +38 -0
- package/dist/lint/rules/index.js +12 -0
- package/dist/lint/rules/postgres-templates.js +27 -0
- package/dist/lint/rules/sqlch001.js +79 -0
- package/dist/lint/rules/sqlch002.js +124 -0
- package/dist/lint/rules/sqlch003.js +69 -0
- package/dist/lint/rules/sqlch004.js +83 -0
- package/dist/lint/rules/sqlpg001.js +68 -0
- package/dist/lint/rules/sqlpg002.js +69 -0
- package/dist/lint/rules/sqlpg003.js +58 -0
- package/dist/lint/rules/sqlpg004.js +77 -0
- package/dist/lint/rules/sqlpg005.js +86 -0
- package/dist/lint/rules/templates.js +25 -0
- package/dist/lint/rules/typed-names.js +132 -0
- package/dist/live-dialect.js +32 -0
- package/dist/lsp/catalog.js +44 -0
- package/dist/lsp/completions.js +136 -0
- package/dist/lsp/hover.js +115 -0
- package/dist/lsp/postgres-catalog.js +176 -0
- package/dist/lsp/postgres-template.js +171 -0
- package/dist/lsp/postgres.js +266 -0
- package/dist/lsp/refs.js +60 -0
- package/dist/lsp/registry.d.ts.map +1 -1
- package/dist/lsp/registry.js +19 -0
- package/dist/lsp/template.js +143 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +293 -0
- package/dist/mcp/postgres.js +117 -0
- package/dist/migration-statements.js +243 -0
- package/dist/op/activities/clickhouse-apply.js +83 -0
- package/dist/op/activities/clickhouse-emulator.js +53 -0
- package/dist/op/activities/clickhouse-rebuild.js +119 -0
- package/dist/op/activities/index.js +26 -0
- package/dist/op/activities/postgres-apply.js +126 -0
- package/dist/op/activities/postgres-emulator.js +70 -0
- package/dist/op/activities/postgres-migration.js +156 -0
- package/dist/op/activities/sql-exec.js +57 -0
- package/dist/op/activity-contracts.js +103 -0
- package/dist/package-cli.js +14 -0
- package/dist/plugin.js +259 -0
- package/dist/postgres/access/acl.js +344 -0
- package/dist/postgres/access/changes.js +30 -0
- package/dist/postgres/access/import.js +61 -0
- package/dist/postgres/access/live.js +96 -0
- package/dist/postgres/apply/apply.js +557 -0
- package/dist/postgres/apply/statements.js +882 -0
- package/dist/postgres/catalog-types.js +18 -0
- package/dist/postgres/container.js +58 -0
- package/dist/postgres/entities.js +1390 -0
- package/dist/postgres/entity-types.js +28 -0
- package/dist/postgres/import/generator.js +283 -0
- package/dist/postgres/import/ir.js +53 -0
- package/dist/postgres/import/live-export.js +83 -0
- package/dist/postgres/keywords.js +32 -0
- package/dist/postgres/live/bind.js +130 -0
- package/dist/postgres/live/catalog-kinds.js +301 -0
- package/dist/postgres/live/catalog.js +644 -0
- package/dist/postgres/live/client.js +151 -0
- package/dist/postgres/live/describe-resources.js +135 -0
- package/dist/postgres/live/undeclarable.js +111 -0
- package/dist/postgres/migrate/backfill.js +170 -0
- package/dist/postgres/migrate/batches.js +122 -0
- package/dist/postgres/migrate/carry.js +518 -0
- package/dist/postgres/migrate/handoff.js +59 -0
- package/dist/postgres/migrate/names.js +61 -0
- package/dist/postgres/migrate/observe.js +360 -0
- package/dist/postgres/migrate/op.js +178 -0
- package/dist/postgres/migrate/receipts.js +117 -0
- package/dist/postgres/migrate/replication.js +106 -0
- package/dist/postgres/migrate/steps.js +514 -0
- package/dist/postgres/overlays/columns.js +31 -0
- package/dist/postgres/overlays/constraints.js +52 -0
- package/dist/postgres/overlays/extension-objects.js +88 -0
- package/dist/postgres/overlays/indexes.js +25 -0
- package/dist/postgres/overlays/kinds.js +2 -0
- package/dist/postgres/overlays/partitioning.js +23 -0
- package/dist/postgres/overlays/relations.js +25 -0
- package/dist/postgres/overlays/storage.js +65 -0
- package/dist/postgres/overlays/type-ddl.js +36 -0
- package/dist/postgres/overlays/types.js +74 -0
- package/dist/postgres/parser.js +1824 -0
- package/dist/postgres/plan/commands.js +137 -0
- package/dist/postgres/plan/deep.js +324 -0
- package/dist/postgres/plan/diff.js +499 -0
- package/dist/postgres/plan/disruption.js +64 -0
- package/dist/postgres/plan/normalize.js +889 -0
- package/dist/postgres/plan/report.js +21 -0
- package/dist/postgres/plan/rules.js +115 -0
- package/dist/postgres/plan/schema.js +105 -0
- package/dist/postgres/plan/server-normalize.js +166 -0
- package/dist/postgres/providers/aurora.js +44 -0
- package/dist/postgres/providers/azure.js +53 -0
- package/dist/postgres/providers/cloud-sql.js +41 -0
- package/dist/postgres/providers/index.js +73 -0
- package/dist/postgres/providers/neon.js +44 -0
- package/dist/postgres/providers/rds.js +47 -0
- package/dist/postgres/providers/supabase.js +45 -0
- package/dist/postgres/providers/types.js +21 -0
- package/dist/postgres/row-types.js +107 -0
- package/dist/postgres/testing/fake-client.js +34 -0
- package/dist/postgres/testing/migration.js +48 -0
- package/dist/postgres/testing/roundtrip-schema.js +64 -0
- package/dist/postgres/testing/server.js +47 -0
- package/dist/postgres/testing/writable-server.js +160 -0
- package/dist/postgres/tokens.js +41 -0
- package/dist/postgres.js +15 -0
- package/dist/read/clickhouse.js +82 -0
- package/dist/read/objects.js +44 -0
- package/dist/read/postgres.js +104 -0
- package/dist/read.js +52 -0
- package/dist/receipts.js +207 -0
- package/dist/serializer.js +172 -0
- package/dist/skill-defs.js +93 -0
- package/dist/spec/catalog.js +191 -0
- package/dist/spec/fetch.d.ts.map +1 -1
- package/dist/spec/fetch.js +90 -0
- package/dist/spec/pin.js +42 -0
- package/dist/spec/postgres-catalog.js +171 -0
- package/dist/spec/postgres-fetch.js +91 -0
- package/dist/spec/postgres-pin.js +82 -0
- package/dist/sql-files.js +206 -0
- package/dist/sql-types-command.js +97 -0
- package/dist/sql-types.js +323 -0
- package/dist/token-source.js +156 -0
- package/dist/validate-cli.js +5 -0
- package/dist/validate.js +151 -0
- package/dist/workspace-outputs.js +191 -0
- package/package.json +7 -7
- package/src/codegen/package.ts +2 -2
- package/src/lint/rules/typescript-7.test.ts +2 -1
- package/src/lsp/catalog.ts +2 -2
- package/src/lsp/postgres-catalog.ts +2 -2
- package/src/lsp/registry.ts +2 -1
- package/src/mcp/index.ts +4 -2
- package/src/spec/fetch.ts +2 -2
- package/src/spec/postgres-fetch.ts +2 -2
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ReplacingTable`: a ReplacingMergeTree table with a version column.
|
|
3
|
+
*
|
|
4
|
+
* ReplacingMergeTree keeps one row per sort key. When a merge meets rows with
|
|
5
|
+
* the same key it keeps the one with the highest version, so writing a row
|
|
6
|
+
* again with a higher version is how an update is expressed. Until a merge
|
|
7
|
+
* runs, a query sees every version; read with `FINAL`, or aggregate with
|
|
8
|
+
* `argMax`, when the latest row is what you want.
|
|
9
|
+
*
|
|
10
|
+
* Every field is built from a parameter or fixed by the template, inside the
|
|
11
|
+
* subset `chant build` interprets, so a drift on the table is reported against
|
|
12
|
+
* the parameter that produced the field when the composite is interpreted.
|
|
13
|
+
*/
|
|
14
|
+
import { Composite } from "@intentius/chant/composite";
|
|
15
|
+
import { table } from "../clickhouse/entities.js";
|
|
16
|
+
/** A ReplacingMergeTree table that keeps the highest version of each row, with its version column. */
|
|
17
|
+
export const ReplacingTable = Composite((props) => {
|
|
18
|
+
const version = props.version ?? "version";
|
|
19
|
+
const replacing = table `
|
|
20
|
+
CREATE TABLE ${props.name} (
|
|
21
|
+
${props.columns},
|
|
22
|
+
${version} ${props.versionType ?? "UInt64"}
|
|
23
|
+
)
|
|
24
|
+
ENGINE = ReplacingMergeTree(${version})
|
|
25
|
+
${props.partitionBy ? `PARTITION BY ${props.partitionBy}` : ""}
|
|
26
|
+
ORDER BY ${props.orderBy}`;
|
|
27
|
+
return { table: replacing };
|
|
28
|
+
}, "ReplacingTable");
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RollupView`: a materialized view that rolls a source table up into its own target table.
|
|
3
|
+
*
|
|
4
|
+
* The target is a SummingMergeTree (or another engine named by `engine`)
|
|
5
|
+
* sorted by the rollup's grouping key. The materialized view writes into it
|
|
6
|
+
* with `TO`, so the target is an ordinary table that can be queried, altered
|
|
7
|
+
* and backfilled on its own, and dropping the view leaves the data. Every
|
|
8
|
+
* column the select list writes has to be one the target declares
|
|
9
|
+
* (SQLCH110), so alias each select item to a target column.
|
|
10
|
+
*
|
|
11
|
+
* The view reads `source` by reference, so it depends on the source and on
|
|
12
|
+
* its target, and the build orders all three.
|
|
13
|
+
*/
|
|
14
|
+
import { Composite } from "@intentius/chant/composite";
|
|
15
|
+
import { table, view } from "../clickhouse/entities.js";
|
|
16
|
+
/** A materialized view that rolls a source table up into a target table it declares beside it. */
|
|
17
|
+
export const RollupView = Composite((props) => {
|
|
18
|
+
const target = table `
|
|
19
|
+
CREATE TABLE ${props.name} (${props.columns})
|
|
20
|
+
ENGINE = ${props.engine ?? "SummingMergeTree"}
|
|
21
|
+
${props.partitionBy ? `PARTITION BY ${props.partitionBy}` : ""}
|
|
22
|
+
ORDER BY ${props.orderBy ?? `(${props.groupBy})`}`;
|
|
23
|
+
const rollup = view `
|
|
24
|
+
CREATE MATERIALIZED VIEW ${`${props.name}_mv`} TO ${target} AS
|
|
25
|
+
SELECT ${props.select}
|
|
26
|
+
FROM ${props.source}
|
|
27
|
+
GROUP BY ${props.groupBy}`;
|
|
28
|
+
return { table: target, view: rollup };
|
|
29
|
+
}, "RollupView");
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ShardedTable`: a ReplicatedMergeTree table on every node of a cluster and the Distributed table that spreads reads and writes across it.
|
|
3
|
+
*
|
|
4
|
+
* Both tables are created `ON CLUSTER`. The local table, `<name>_local`, holds
|
|
5
|
+
* each shard's rows and replicates them within the shard; with no engine
|
|
6
|
+
* arguments it takes the server's default replication path. The Distributed
|
|
7
|
+
* table, `<name>`, holds no data: an insert goes to one shard by the sharding
|
|
8
|
+
* key, and a query runs on every shard and merges the results.
|
|
9
|
+
*
|
|
10
|
+
* The Distributed table reads the local one by reference, so the build creates
|
|
11
|
+
* the local table first. Give `name` without a database: the Distributed
|
|
12
|
+
* engine names its table in the current database.
|
|
13
|
+
*/
|
|
14
|
+
import { Composite } from "@intentius/chant/composite";
|
|
15
|
+
import { table } from "../clickhouse/entities.js";
|
|
16
|
+
/** A ReplicatedMergeTree table on every node of a cluster and the Distributed table over it. */
|
|
17
|
+
export const ShardedTable = Composite((props) => {
|
|
18
|
+
const local = table `
|
|
19
|
+
CREATE TABLE ${`${props.name}_local`} ON CLUSTER ${props.cluster} (${props.columns})
|
|
20
|
+
ENGINE = ReplicatedMergeTree
|
|
21
|
+
${props.partitionBy ? `PARTITION BY ${props.partitionBy}` : ""}
|
|
22
|
+
ORDER BY ${props.orderBy}`;
|
|
23
|
+
const distributed = table `
|
|
24
|
+
CREATE TABLE ${props.name} ON CLUSTER ${props.cluster} (${props.columns})
|
|
25
|
+
ENGINE = Distributed(${props.cluster}, currentDatabase(), ${local}, ${props.shardingKey ?? "rand()"})`;
|
|
26
|
+
return { local, distributed };
|
|
27
|
+
}, "ShardedTable");
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SoftDeleteTable`: a table whose rows carry created and updated timestamps and are deleted by marking them.
|
|
3
|
+
*
|
|
4
|
+
* The composite appends `created_at` and `updated_at` (both `timestamptz NOT
|
|
5
|
+
* NULL DEFAULT now()`) and a nullable `deleted_at` to the columns it is
|
|
6
|
+
* given. A row is live while `deleted_at` is null. The `<name>_live` view
|
|
7
|
+
* reads the live rows (it sets `security_invoker`, so row-level security on
|
|
8
|
+
* the table applies to whoever reads the view), and a partial index on the
|
|
9
|
+
* live rows keeps lookups by the key the app uses off the dead ones.
|
|
10
|
+
*
|
|
11
|
+
* Postgres has no column default that follows an update: the app, or a
|
|
12
|
+
* trigger it owns, sets `updated_at`.
|
|
13
|
+
*/
|
|
14
|
+
import { Composite } from "@intentius/chant/composite";
|
|
15
|
+
import { index, literal, table, view } from "../postgres/entities.js";
|
|
16
|
+
/** A table with created and updated timestamps and a soft-delete column, a view of its live rows and an index over them. */
|
|
17
|
+
export const SoftDeleteTable = Composite((props) => {
|
|
18
|
+
const schema = props.schema ?? "public";
|
|
19
|
+
const deleted = props.deletedAt ?? "deleted_at";
|
|
20
|
+
const rows = table `
|
|
21
|
+
CREATE TABLE ${schema}.${props.name} (
|
|
22
|
+
${props.columns},
|
|
23
|
+
${props.createdAt ?? "created_at"} timestamptz NOT NULL DEFAULT now(),
|
|
24
|
+
${props.updatedAt ?? "updated_at"} timestamptz NOT NULL DEFAULT now(),
|
|
25
|
+
${deleted} timestamptz
|
|
26
|
+
);
|
|
27
|
+
COMMENT ON TABLE ${schema}.${props.name} IS ${literal(props.comment ?? "Soft-deleted: a row is live while its deletion timestamp is null")}`;
|
|
28
|
+
const live = view `
|
|
29
|
+
CREATE VIEW ${schema}.${`${props.name}_live`} WITH (security_invoker = true) AS
|
|
30
|
+
SELECT * FROM ${rows} WHERE ${deleted} IS NULL`;
|
|
31
|
+
const liveIndex = index `
|
|
32
|
+
CREATE INDEX ${`${props.name}_live_idx`} ON ${rows} (${props.liveKey ?? "id"}) WHERE ${deleted} IS NULL`;
|
|
33
|
+
return { table: rows, live, liveIndex };
|
|
34
|
+
}, "SoftDeleteTable");
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TenantTable`: a multi-tenant table whose tenant key leads every key and index.
|
|
3
|
+
*
|
|
4
|
+
* The composite adds the tenant column (`NOT NULL`) in front of the columns it
|
|
5
|
+
* is given, makes the primary key `(<tenant>, <primaryKey>)` and creates one
|
|
6
|
+
* index on `(<tenant>, <indexOn>)`. With the tenant first in each, a query
|
|
7
|
+
* scoped to one tenant reads one contiguous range of every index, and no index
|
|
8
|
+
* serves a cross-tenant scan by accident. Row-level security is separate: a
|
|
9
|
+
* policy on the table is what keeps one tenant's role out of another's rows.
|
|
10
|
+
*/
|
|
11
|
+
import { Composite } from "@intentius/chant/composite";
|
|
12
|
+
import { index, table } from "../postgres/entities.js";
|
|
13
|
+
/** A multi-tenant table with the tenant key first in its primary key and its index. */
|
|
14
|
+
export const TenantTable = Composite((props) => {
|
|
15
|
+
const schema = props.schema ?? "public";
|
|
16
|
+
const tenant = props.tenant ?? "tenant_id";
|
|
17
|
+
const rows = table `
|
|
18
|
+
CREATE TABLE ${schema}.${props.name} (
|
|
19
|
+
${tenant} ${props.tenantType ?? "uuid"} NOT NULL,
|
|
20
|
+
${props.columns},
|
|
21
|
+
PRIMARY KEY (${tenant}, ${props.primaryKey})
|
|
22
|
+
)`;
|
|
23
|
+
const byTenant = index `
|
|
24
|
+
CREATE INDEX ${`${props.name}_tenant_idx`} ON ${rows} (${tenant}, ${props.indexOn})`;
|
|
25
|
+
return { table: rows, index: byTenant };
|
|
26
|
+
}, "TenantTable");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chant#3787 — the call signature a composite is typed with, so a member's
|
|
3
|
+
* `.columns` are the generated `SqlSchema` entry for the name its props give
|
|
4
|
+
* it. `users.table.columns.emial` then fails tsc as `table<"app.users">`'s
|
|
5
|
+
* would. The definition is unchanged at run time: `Composite`'s third type
|
|
6
|
+
* argument carries this signature in front of the plain one.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `sql` namespace in `chant.config.ts`.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* import type { ChantConfig } from "@intentius/chant/config";
|
|
6
|
+
* import "@intentius/chant-lexicon-sql"; // brings the `sql` key into ChantConfig
|
|
7
|
+
*
|
|
8
|
+
* export default {
|
|
9
|
+
* lexicons: ["sql"],
|
|
10
|
+
* sql: {
|
|
11
|
+
* dialect: "clickhouse",
|
|
12
|
+
* profiles: {
|
|
13
|
+
* staging: { url: "http://clickhouse.staging:8123", user: { env: "CH_USER" }, password: { env: "CH_PASSWORD" } },
|
|
14
|
+
* prod: { url: "https://clickhouse.example.com:8443", user: { env: "CH_USER" }, password: { env: "CH_PROD_PASSWORD" }, databases: ["analytics"] },
|
|
15
|
+
* },
|
|
16
|
+
* },
|
|
17
|
+
* } satisfies ChantConfig;
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* A profile is keyed by chant environment, as `grafana.profiles.<env>` is:
|
|
21
|
+
* the server `chant import --from <env>`, `chant lifecycle diff <env> --live`
|
|
22
|
+
* and `plan` read. Credentials are named by their environment variable, never
|
|
23
|
+
* written in the config, or minted as a short-lived token (`password:
|
|
24
|
+
* { token: "rds-iam" }`; `./token-source.ts`). An environment with no profile falls back to
|
|
25
|
+
* `CLICKHOUSE_URL`, `CLICKHOUSE_USER` and `CLICKHOUSE_PASSWORD`, or for
|
|
26
|
+
* Postgres `POSTGRES_URL`, `POSTGRES_USER` and `POSTGRES_PASSWORD`. A profile
|
|
27
|
+
* whose `url` is `postgres://` or `postgresql://` is a Postgres server.
|
|
28
|
+
*
|
|
29
|
+
* `dialect` names the database a project's schema is for. Declarations carry
|
|
30
|
+
* their dialect in their own type (a ClickHouse table is `ClickHouse::Table`),
|
|
31
|
+
* so the setting is for the things a declaration cannot answer: which dialect
|
|
32
|
+
* a live environment is read as on import, and which dialect's checks run on
|
|
33
|
+
* output that names none. It defaults to `clickhouse`, the first dialect.
|
|
34
|
+
* `postgres` is the second (#3289). A list (`dialect: ["clickhouse",
|
|
35
|
+
* "postgres"]`) is accepted for a workspace with members of more than one
|
|
36
|
+
* dialect (#3047 question 1); one build still holds one dialect.
|
|
37
|
+
*
|
|
38
|
+
* `provider` (#3282) names the managed Postgres service, on the namespace or
|
|
39
|
+
* on one profile: `sql: { dialect: "postgres", provider: "rds" }`, or
|
|
40
|
+
* `profiles: { prod: { url, provider: "aurora" } }`. A provider's data lives
|
|
41
|
+
* in `@intentius/chant-lexicon-sql/postgres` (`providerData`).
|
|
42
|
+
*/
|
|
43
|
+
import { z } from "zod";
|
|
44
|
+
import { PLANNED_SQL_DIALECTS, SQL_DIALECTS } from "./dialects.js";
|
|
45
|
+
import { POSTGRES_PROVIDERS } from "./postgres/providers/types.js";
|
|
46
|
+
import { POSTGRES_MAJORS } from "./spec/postgres-pin.js";
|
|
47
|
+
import { parseTopology } from "./clickhouse/topology.js";
|
|
48
|
+
import { tokenSourceSchema } from "./token-source.js";
|
|
49
|
+
const envRef = z.strictObject({ env: z.string() });
|
|
50
|
+
/** The managed Postgres service an environment runs on (#3282). */
|
|
51
|
+
const providerName = z.enum(POSTGRES_PROVIDERS, {
|
|
52
|
+
error: () => `expected a Postgres provider (${POSTGRES_PROVIDERS.join(", ")})`,
|
|
53
|
+
});
|
|
54
|
+
/** A ClickHouse topology, in its string form or its object form (`./clickhouse/topology.ts`). */
|
|
55
|
+
const topologySchema = z.union([
|
|
56
|
+
z.string().refine((v) => {
|
|
57
|
+
try {
|
|
58
|
+
parseTopology(v);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}, { error: "expected single, cluster:<name>, replicated, replicated:<cluster> or cloud" }),
|
|
65
|
+
z.discriminatedUnion("kind", [
|
|
66
|
+
z.strictObject({ kind: z.literal("single") }),
|
|
67
|
+
z.strictObject({ kind: z.literal("cluster"), cluster: z.string().min(1), replicaPath: z.string().optional(), replicaName: z.string().optional() }),
|
|
68
|
+
z.strictObject({ kind: z.literal("replicated"), cluster: z.string().min(1).optional() }),
|
|
69
|
+
z.strictObject({ kind: z.literal("cloud") }),
|
|
70
|
+
]),
|
|
71
|
+
]);
|
|
72
|
+
export const sqlProfileSchema = z.strictObject({
|
|
73
|
+
/**
|
|
74
|
+
* Where the server is. ClickHouse: the base URL of its HTTP interface
|
|
75
|
+
* (`http://clickhouse:8123`). Postgres: a connection URL
|
|
76
|
+
* (`postgres://db.internal:5432/shop`), with no password in it; the scheme
|
|
77
|
+
* is what makes a profile a Postgres one.
|
|
78
|
+
*/
|
|
79
|
+
url: z.string(),
|
|
80
|
+
/** The user, named by its environment variable. `default` when omitted. */
|
|
81
|
+
user: envRef.optional(),
|
|
82
|
+
/**
|
|
83
|
+
* The password, named by its environment variable, or a token source that
|
|
84
|
+
* mints a short-lived one when a connection needs it (#3685):
|
|
85
|
+
* `{ token: "rds-iam" }`, `{ token: "cloud-sql-iam" }`, `{ token: "entra" }`
|
|
86
|
+
* (Postgres), or `{ token: "command", command: ["./mint.sh"] }`, whose
|
|
87
|
+
* standard output is the password. A token is minted again before it
|
|
88
|
+
* expires. See `./token-source.ts`.
|
|
89
|
+
*/
|
|
90
|
+
password: z.union([envRef, tokenSourceSchema]).optional(),
|
|
91
|
+
/**
|
|
92
|
+
* The databases this environment's schema lives in. Import reads these and
|
|
93
|
+
* nothing else; when omitted, every database except the server's own
|
|
94
|
+
* (`system`, `information_schema`, `INFORMATION_SCHEMA`).
|
|
95
|
+
*/
|
|
96
|
+
databases: z.array(z.string()).optional(),
|
|
97
|
+
/** The database an unqualified declaration is created in. `default` when omitted. */
|
|
98
|
+
defaultDatabase: z.string().optional(),
|
|
99
|
+
/**
|
|
100
|
+
* ClickHouse import (#3718): the SQL user-defined functions `chant import
|
|
101
|
+
* --from <env>` adopts besides those the imported objects use, by name, or
|
|
102
|
+
* by prefix with a trailing `*` (`shop_*`). A function belongs to no
|
|
103
|
+
* database, so without this only the functions the imported tables, views
|
|
104
|
+
* and dictionaries use (and the functions those call) are imported. A use
|
|
105
|
+
* is found by name or, since ClickHouse stores a call as the function's
|
|
106
|
+
* body, by that body (#3745).
|
|
107
|
+
*/
|
|
108
|
+
importFunctions: z.array(z.string()).optional(),
|
|
109
|
+
/**
|
|
110
|
+
* ClickHouse: the topology this environment runs, which the applier, the
|
|
111
|
+
* plan and the rebuild migration render every statement for: `single`,
|
|
112
|
+
* `cluster:<name>` (`ON CLUSTER`, `Replicated*MergeTree` with a Keeper
|
|
113
|
+
* path), `replicated` (a `Replicated` database; `replicated:<cluster>`
|
|
114
|
+
* creates the databases on that cluster) or `cloud`. The object form sets
|
|
115
|
+
* a cluster's Keeper path and replica name:
|
|
116
|
+
* `{ kind: "cluster", cluster: "main", replicaPath, replicaName }`. When
|
|
117
|
+
* omitted, statements are sent as declared.
|
|
118
|
+
*/
|
|
119
|
+
topology: topologySchema.optional(),
|
|
120
|
+
/**
|
|
121
|
+
* Postgres: the schemas this environment's schema lives in. Import and
|
|
122
|
+
* observation read these and nothing else; when omitted, every schema except
|
|
123
|
+
* the server's own (`pg_catalog`, `information_schema`, `pg_toast`, the temp
|
|
124
|
+
* schemas).
|
|
125
|
+
*/
|
|
126
|
+
schemas: z.array(z.string()).optional(),
|
|
127
|
+
/** Postgres: the schema an unqualified declaration is created in. `public` when omitted. */
|
|
128
|
+
defaultSchema: z.string().optional(),
|
|
129
|
+
/**
|
|
130
|
+
* Postgres: the schema `effect()` receipts are kept in, as
|
|
131
|
+
* `<schema>.__chant_receipts`. When omitted they are in
|
|
132
|
+
* `chant_receipts.receipts`, a schema chant creates; name an existing schema
|
|
133
|
+
* on a server where the role cannot `CREATE SCHEMA`.
|
|
134
|
+
*/
|
|
135
|
+
receiptsSchema: z.string().optional(),
|
|
136
|
+
/**
|
|
137
|
+
* The managed service this environment's Postgres runs on, when it differs
|
|
138
|
+
* from `sql.provider`. Import reads the provider's own roles, schemas and
|
|
139
|
+
* extensions as foreign.
|
|
140
|
+
*/
|
|
141
|
+
provider: providerName.optional(),
|
|
142
|
+
/**
|
|
143
|
+
* Postgres apply: how long one statement waits for its lock before it
|
|
144
|
+
* fails (`lock_timeout`), in milliseconds, so a change blocked behind a
|
|
145
|
+
* long query fails fast instead of queueing every query behind it. The
|
|
146
|
+
* catalog reads (`chant lifecycle diff --live`, `chant sql plan`, the
|
|
147
|
+
* import, and an apply's read before its first statement) wait under it
|
|
148
|
+
* too, and name the session holding the lock when it passes. 5000 when
|
|
149
|
+
* omitted.
|
|
150
|
+
*/
|
|
151
|
+
lockTimeoutMs: z.number().int().nonnegative().optional(),
|
|
152
|
+
/**
|
|
153
|
+
* Postgres apply: the `statement_timeout` for a statement that changes only
|
|
154
|
+
* the catalog, in milliseconds. 60000 when omitted.
|
|
155
|
+
*/
|
|
156
|
+
statementTimeoutMs: z.number().int().nonnegative().optional(),
|
|
157
|
+
/**
|
|
158
|
+
* Postgres apply: the `statement_timeout` for a statement that reads or
|
|
159
|
+
* rewrites every row (a table rewrite, a constraint validation, a
|
|
160
|
+
* `CONCURRENTLY` index build), in milliseconds. 0, no limit, when omitted.
|
|
161
|
+
*/
|
|
162
|
+
scanTimeoutMs: z.number().int().nonnegative().optional(),
|
|
163
|
+
/**
|
|
164
|
+
* Whether chant manages access in this environment. Postgres (#3681): the
|
|
165
|
+
* declared row-level security policies and each table's row-level security,
|
|
166
|
+
* the declared roles (created when missing, never dropped), and the
|
|
167
|
+
* privileges on the declared objects and the default privileges, which a
|
|
168
|
+
* plan compares and an apply grants and revokes. ClickHouse (#3716): the
|
|
169
|
+
* declared users, roles, row policies and grants. Off when omitted: the
|
|
170
|
+
* access declarations are not planned or applied, and the server's access
|
|
171
|
+
* is not read, so a project that manages access elsewhere is not affected.
|
|
172
|
+
*/
|
|
173
|
+
access: z.boolean().optional(),
|
|
174
|
+
});
|
|
175
|
+
const dialectName = z.enum(SQL_DIALECTS);
|
|
176
|
+
/** Why a `dialect` value is refused: a planned dialect is "not implemented yet", anything else unknown. */
|
|
177
|
+
function dialectError(input) {
|
|
178
|
+
const names = Array.isArray(input) ? input : [input];
|
|
179
|
+
const planned = names.find((n) => typeof n === "string" && PLANNED_SQL_DIALECTS.includes(n));
|
|
180
|
+
if (planned !== undefined) {
|
|
181
|
+
return `the ${planned} dialect is not implemented yet; the implemented dialects are ${SQL_DIALECTS.join(", ")}`;
|
|
182
|
+
}
|
|
183
|
+
return `expected a dialect (${SQL_DIALECTS.join(", ")}) or a non-empty list of them`;
|
|
184
|
+
}
|
|
185
|
+
export const sqlConfigSchema = z.strictObject({
|
|
186
|
+
/** The dialect, or the dialects, the project's schema is for. */
|
|
187
|
+
dialect: z.union([dialectName, z.array(dialectName).min(1)], { error: (iss) => dialectError(iss.input) }).optional(),
|
|
188
|
+
/**
|
|
189
|
+
* The managed Postgres service the project deploys to (`rds`, `aurora`,
|
|
190
|
+
* `cloud-sql`, `azure`, `neon`, `supabase`). SQLPG004 reads it to refuse an
|
|
191
|
+
* extension the provider does not allow. Omit it for a self-hosted server.
|
|
192
|
+
*/
|
|
193
|
+
provider: providerName.optional(),
|
|
194
|
+
/**
|
|
195
|
+
* The Postgres major the project targets. The editor completes and hovers
|
|
196
|
+
* only what that major has (a function added in 18 is not offered at 16);
|
|
197
|
+
* without it, the newest supported major is used.
|
|
198
|
+
*/
|
|
199
|
+
postgresMajor: z
|
|
200
|
+
.number()
|
|
201
|
+
.int()
|
|
202
|
+
.refine((n) => POSTGRES_MAJORS.includes(n), { error: `expected a supported Postgres major (${POSTGRES_MAJORS.join(", ")})` })
|
|
203
|
+
.optional(),
|
|
204
|
+
/** One server per chant environment. */
|
|
205
|
+
profiles: z.record(z.string(), sqlProfileSchema).optional(),
|
|
206
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The applier's plumbing, the same in every dialect (lexicon-authoring/
|
|
3
|
+
* applying.mdx): reading a build's output, the outcome's tri-state (applied,
|
|
4
|
+
* not attempted, and failed for a statement the server refused), its
|
|
5
|
+
* projection onto core's apply envelope, and the ownership marker an apply
|
|
6
|
+
* stamps and prunes by.
|
|
7
|
+
*
|
|
8
|
+
* What a dialect's applier decides for itself: the statements a change takes,
|
|
9
|
+
* which changes it refuses and why, how it waits for the server, and how a
|
|
10
|
+
* prune drops an object.
|
|
11
|
+
*/
|
|
12
|
+
import { applyResult, notAttemptedAll, } from "@intentius/chant/apply";
|
|
13
|
+
const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
14
|
+
/**
|
|
15
|
+
* The objects a `chant build` output declares for `dialect`, in creation
|
|
16
|
+
* order, each passed through `map`. Takes the sql lexicon's primary output,
|
|
17
|
+
* or a multi-lexicon output holding it under `sql`.
|
|
18
|
+
*/
|
|
19
|
+
export function readBuildObjects(json, dialect, map) {
|
|
20
|
+
const raw = JSON.parse(json);
|
|
21
|
+
const doc = isObject(raw) && isObject(raw.sql) ? raw.sql : raw;
|
|
22
|
+
if (!isObject(doc) || doc.dialect !== dialect || !Array.isArray(doc.objects)) {
|
|
23
|
+
throw new Error(`not a sql lexicon build output: expected { dialect: "${dialect}", objects: [...] }`);
|
|
24
|
+
}
|
|
25
|
+
return doc.objects.map((o, i) => {
|
|
26
|
+
if (!isObject(o) || typeof o.export !== "string" || typeof o.ddl !== "string" || typeof o.type !== "string") {
|
|
27
|
+
throw new Error(`sql build output: objects[${i}] has no export, type or ddl`);
|
|
28
|
+
}
|
|
29
|
+
const dependsOn = [...new Set((Array.isArray(o.dependsOn) ? o.dependsOn : []).filter((d) => typeof d === "string").map((d) => d.split(".")[0]))];
|
|
30
|
+
return map({ export: o.export, type: o.type, ddl: o.ddl, dependsOn }, i);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* An apply in which the server refused a statement. The envelope has no
|
|
35
|
+
* bucket for "attempted and not converged", so the applier finishes the rest
|
|
36
|
+
* and throws this, carrying the outcome.
|
|
37
|
+
*/
|
|
38
|
+
export class SqlApplyError extends Error {
|
|
39
|
+
constructor(
|
|
40
|
+
/** The dialect as a message names it: `ClickHouse`. */
|
|
41
|
+
dialect, outcome) {
|
|
42
|
+
super(`${dialect} apply to ${outcome.target}: ${outcome.failed.length} object(s) failed ` +
|
|
43
|
+
`(${outcome.failed.map((f) => `${f.kind}/${f.name}: ${f.error}`).join("; ")}); ` +
|
|
44
|
+
`${outcome.applied.length} applied, ${outcome.pruned.length} pruned, ${outcome.notAttempted.length} not attempted before and after`);
|
|
45
|
+
this.outcome = outcome;
|
|
46
|
+
this.name = "SqlApplyError";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** An outcome in which nothing was attempted: every object not attempted with one reason. */
|
|
50
|
+
export function notAttemptedOutcome(refs, reason, detail) {
|
|
51
|
+
return { target: "", source: "", applied: [], pruned: [], notAttempted: notAttemptedAll(refs, reason, detail), failed: [] };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Project an apply outcome onto core's apply envelope. The outcome keeps what
|
|
55
|
+
* the envelope has no room for: the statements each object took and the
|
|
56
|
+
* server they went to.
|
|
57
|
+
*/
|
|
58
|
+
export function toApplyResult(outcome) {
|
|
59
|
+
return applyResult(outcome.applied.map((a) => ({ kind: a.kind, name: a.name, action: a.action, ...(a.physicalId ? { physicalId: a.physicalId } : {}) })), outcome.pruned.map((p) => ({ kind: p.kind, name: p.name, deleted: p.deleted })), outcome.notAttempted.map((n) => ({ kind: n.kind, name: n.name, reason: n.reason, ...(n.detail ? { detail: n.detail } : {}) })));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The marker to stamp and prune by: `stack` and `ownershipEnv` when passed,
|
|
63
|
+
* else the project's `ownership` config. Undefined without a stack. `who`
|
|
64
|
+
* names the caller in the error for an env an apply cannot resolve.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveOwnershipMarker(args, config, who) {
|
|
67
|
+
const o = config?.ownership;
|
|
68
|
+
const configured = o && o.enabled !== false ? o : undefined;
|
|
69
|
+
const stack = args.stack ?? configured?.stack;
|
|
70
|
+
if (!stack)
|
|
71
|
+
return undefined;
|
|
72
|
+
const env = args.ownershipEnv ?? (typeof configured?.env === "string" ? configured.env : undefined);
|
|
73
|
+
if (env === undefined && configured?.env !== undefined) {
|
|
74
|
+
throw new Error(`${who}: ownership.env is a build parameter reference, which an apply cannot resolve; pass ownershipEnv`);
|
|
75
|
+
}
|
|
76
|
+
return { stack, ...(env ? { env } : {}) };
|
|
77
|
+
}
|
|
78
|
+
/** Changes grouped by the object they change, in order. */
|
|
79
|
+
export function changesByObject(changes) {
|
|
80
|
+
const byObject = new Map();
|
|
81
|
+
for (const c of changes)
|
|
82
|
+
byObject.set(c.object, [...(byObject.get(c.object) ?? []), c]);
|
|
83
|
+
return byObject;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The declared objects `dependsOn` names that are not on the server: declared
|
|
87
|
+
* in this build, and not created or found in this apply. An object with any
|
|
88
|
+
* is not attempted, `dependency-failed`, with {@link dependencyFailedDetail}.
|
|
89
|
+
*/
|
|
90
|
+
export function missingDependencies(self, dependsOn, declared, onServer) {
|
|
91
|
+
return dependsOn.filter((d) => declared.has(d) && d !== self && !onServer.has(d));
|
|
92
|
+
}
|
|
93
|
+
export const dependencyFailedDetail = (missing) => `references ${missing.join(", ")}, which is not on the server: it failed or was not attempted in this apply`;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The change classifier's framework, the same in every dialect: the shape of
|
|
3
|
+
* a rule, a classified change, `classifyDisruption()` over a dialect's rules,
|
|
4
|
+
* and the text report.
|
|
5
|
+
*
|
|
6
|
+
* The classes are data, one set per dialect. ClickHouse classes a change by
|
|
7
|
+
* whether `ALTER` can make it (metadata, background rewrite, rebuild); another
|
|
8
|
+
* dialect classes by the lock a change takes and whether it rewrites the
|
|
9
|
+
* table. Each dialect declares its classes with {@link ChangeClasses} and its
|
|
10
|
+
* rules with {@link classifierRule}, every rule citing the database's own
|
|
11
|
+
* documentation for the restriction behind it.
|
|
12
|
+
*/
|
|
13
|
+
export const classifierRule = (id, cls, title, restriction, cite) => ({
|
|
14
|
+
id,
|
|
15
|
+
class: cls,
|
|
16
|
+
title,
|
|
17
|
+
restriction,
|
|
18
|
+
cite,
|
|
19
|
+
});
|
|
20
|
+
/** A classified change from `before` to `after`, its class read from `rules`. */
|
|
21
|
+
export function classifiedChange(rules, object, field, rule, before, after, extra = {}) {
|
|
22
|
+
return {
|
|
23
|
+
object,
|
|
24
|
+
field,
|
|
25
|
+
...(before !== undefined ? { before: String(before) } : {}),
|
|
26
|
+
...(after !== undefined ? { after: String(after) } : {}),
|
|
27
|
+
rule,
|
|
28
|
+
class: rules[rule].class,
|
|
29
|
+
...extra,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
// ── classifyDisruption() ────────────────────────────────────────────────
|
|
33
|
+
const RANK = { "in-place": 0, rolling: 1, replace: 2, destroy: 3, unknown: 4 };
|
|
34
|
+
/**
|
|
35
|
+
* `classifyDisruption()` over a dialect's rules: each update's disruption is
|
|
36
|
+
* the worst class among its changed paths, and `unknown` when any path cannot
|
|
37
|
+
* be classified from the path alone.
|
|
38
|
+
*/
|
|
39
|
+
export function classifyDisruptionWith(classifier, options) {
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const q of options.changes) {
|
|
42
|
+
if (q.type !== undefined && !q.type.startsWith(classifier.typePrefix))
|
|
43
|
+
continue;
|
|
44
|
+
let level = "in-place";
|
|
45
|
+
const because = [];
|
|
46
|
+
const rules = new Set();
|
|
47
|
+
let ambiguous = false;
|
|
48
|
+
for (const d of q.deltas) {
|
|
49
|
+
const r = classifier.ruleFor(d.path);
|
|
50
|
+
if (r === undefined || r === "ambiguous") {
|
|
51
|
+
ambiguous = true;
|
|
52
|
+
because.push(d.path);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const l = classifier.classes.disruption[classifier.rules[r].class];
|
|
56
|
+
if (RANK[l] > RANK[level]) {
|
|
57
|
+
level = l;
|
|
58
|
+
because.length = 0;
|
|
59
|
+
}
|
|
60
|
+
if (RANK[l] === RANK[level])
|
|
61
|
+
because.push(d.path);
|
|
62
|
+
rules.add(r);
|
|
63
|
+
}
|
|
64
|
+
if (ambiguous) {
|
|
65
|
+
out[q.name] = { disruption: "unknown", because, detail: classifier.ambiguousDetail };
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
out[q.name] = {
|
|
69
|
+
disruption: level,
|
|
70
|
+
because,
|
|
71
|
+
detail: [...rules].map((r) => `${r} ${classifier.rules[r].title}`).join("; "),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
// ── The report ──────────────────────────────────────────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* A change set as text, for a terminal or a pull request: each object's
|
|
79
|
+
* changes with their class, rule, restriction and citation, then a count per
|
|
80
|
+
* class and the hints. `trailer` is the dialect's closing lines (what a plan
|
|
81
|
+
* refuses and what to run instead); it is left out when nothing changed.
|
|
82
|
+
*/
|
|
83
|
+
export function renderChangeSet(diff, opts) {
|
|
84
|
+
const lines = [];
|
|
85
|
+
if (opts.title)
|
|
86
|
+
lines.push(opts.title, "");
|
|
87
|
+
if (diff.changes.length === 0) {
|
|
88
|
+
lines.push("No changes.");
|
|
89
|
+
return lines.join("\n");
|
|
90
|
+
}
|
|
91
|
+
const byObject = new Map();
|
|
92
|
+
for (const c of diff.changes)
|
|
93
|
+
byObject.set(c.object, [...(byObject.get(c.object) ?? []), c]);
|
|
94
|
+
for (const [object, changes] of byObject) {
|
|
95
|
+
lines.push(object);
|
|
96
|
+
for (const c of changes) {
|
|
97
|
+
const rule = opts.rules[c.rule];
|
|
98
|
+
const values = c.before !== undefined && c.after !== undefined ? `: ${c.before} -> ${c.after}` : c.after !== undefined ? `: ${c.after}` : c.before !== undefined ? `: ${c.before}` : "";
|
|
99
|
+
lines.push(` [${opts.classes.label[c.class]}] ${c.field}${values}`);
|
|
100
|
+
lines.push(` ${c.rule} ${rule.title}${c.destructive ? " (destroys data)" : ""}. ${rule.restriction} ${rule.cite}`);
|
|
101
|
+
if (c.note)
|
|
102
|
+
lines.push(` ${c.note}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const counts = opts.classes.order
|
|
106
|
+
.map((k) => [k, diff.changes.filter((c) => c.class === k).length])
|
|
107
|
+
.filter(([, n]) => n > 0)
|
|
108
|
+
.map(([k, n]) => `${n} ${opts.classes.label[k]}`);
|
|
109
|
+
lines.push("", counts.join(", "));
|
|
110
|
+
for (const h of diff.hints)
|
|
111
|
+
lines.push(`hint: ${h}`);
|
|
112
|
+
if (opts.trailer)
|
|
113
|
+
lines.push(...opts.trailer);
|
|
114
|
+
return lines.join("\n");
|
|
115
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A throwaway database server in a container, for every dialect: started on a
|
|
3
|
+
* random loopback port under a unique name, waited on until it answers, and
|
|
4
|
+
* always removed.
|
|
5
|
+
*
|
|
6
|
+
* Generation uses it on a pin move to read the pinned server's catalog, and
|
|
7
|
+
* the live tests use it so every container chant starts is started and
|
|
8
|
+
* cleaned up one way. What "answers" means (an HTTP ping, a query) and the
|
|
9
|
+
* image's environment are the dialect's.
|
|
10
|
+
*/
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { promisify } from "node:util";
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
14
|
+
const exec = promisify(execFile);
|
|
15
|
+
/** True when a Docker daemon answers. Tests that need a server skip when this is false. */
|
|
16
|
+
export async function dockerAvailable() {
|
|
17
|
+
try {
|
|
18
|
+
await exec("docker", ["info", "--format", "{{.ServerVersion}}"], { timeout: 15000 });
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** A container name no other run picks: `<prefix>-<pid>-<random>`. */
|
|
26
|
+
export function uniqueContainerName(prefix) {
|
|
27
|
+
return `${prefix}-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
28
|
+
}
|
|
29
|
+
/** The loopback host port Docker published for `containerPort` on `name`. */
|
|
30
|
+
export async function publishedPort(name, containerPort) {
|
|
31
|
+
const { stdout } = await exec("docker", ["port", name, `${containerPort}/tcp`]);
|
|
32
|
+
const port = stdout.split("\n")[0]?.trim().split(":").pop();
|
|
33
|
+
if (!port)
|
|
34
|
+
throw new Error(`docker port reported no host port for ${name}`);
|
|
35
|
+
return port;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Run `image` detached, publish `containerPort` on a random loopback port,
|
|
39
|
+
* and wait until `ready(port)` is true, polling every half second. Pulls the
|
|
40
|
+
* image when it is not present, which is the one network step here. On any
|
|
41
|
+
* failure the container is removed before the error is thrown.
|
|
42
|
+
*/
|
|
43
|
+
export async function startScratchContainer(options) {
|
|
44
|
+
const name = uniqueContainerName(options.namePrefix);
|
|
45
|
+
let removed = false;
|
|
46
|
+
const stop = async () => {
|
|
47
|
+
if (removed)
|
|
48
|
+
return;
|
|
49
|
+
removed = true;
|
|
50
|
+
await exec("docker", ["rm", "-f", ...(options.removeVolumes ? ["-v"] : []), name]).catch(() => undefined);
|
|
51
|
+
};
|
|
52
|
+
try {
|
|
53
|
+
const env = Object.entries(options.env ?? {}).flatMap(([k, v]) => ["-e", `${k}=${v}`]);
|
|
54
|
+
await exec("docker", ["run", "-d", "--name", name, "-p", `127.0.0.1::${options.containerPort}`, ...env, options.image], { timeout: 600000 });
|
|
55
|
+
const port = await publishedPort(name, options.containerPort);
|
|
56
|
+
options.onStarted?.(name);
|
|
57
|
+
const deadline = Date.now() + options.readyTimeoutMs;
|
|
58
|
+
while (!(await options.ready(port))) {
|
|
59
|
+
if (Date.now() > deadline)
|
|
60
|
+
throw new Error(`${name} ${options.notReady} within the timeout`);
|
|
61
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
62
|
+
}
|
|
63
|
+
return { name, port, stop };
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
await stop();
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|