@intentius/chant-lexicon-sql 0.124.31 → 0.124.33
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
|
+
* The Postgres entity types, apart from the tags so generation can name them
|
|
3
|
+
* before the generated tables the tags read exist.
|
|
4
|
+
*/
|
|
5
|
+
export const POSTGRES_ENTITY_TYPES = {
|
|
6
|
+
schema: "Postgres::Schema",
|
|
7
|
+
table: "Postgres::Table",
|
|
8
|
+
index: "Postgres::Index",
|
|
9
|
+
view: "Postgres::View",
|
|
10
|
+
materializedView: "Postgres::MaterializedView",
|
|
11
|
+
sequence: "Postgres::Sequence",
|
|
12
|
+
enum: "Postgres::Enum",
|
|
13
|
+
composite: "Postgres::CompositeType",
|
|
14
|
+
range: "Postgres::RangeType",
|
|
15
|
+
domain: "Postgres::Domain",
|
|
16
|
+
aggregate: "Postgres::Aggregate",
|
|
17
|
+
collation: "Postgres::Collation",
|
|
18
|
+
cast: "Postgres::Cast",
|
|
19
|
+
eventTrigger: "Postgres::EventTrigger",
|
|
20
|
+
extension: "Postgres::Extension",
|
|
21
|
+
function: "Postgres::Function",
|
|
22
|
+
procedure: "Postgres::Procedure",
|
|
23
|
+
trigger: "Postgres::Trigger",
|
|
24
|
+
policy: "Postgres::Policy",
|
|
25
|
+
role: "Postgres::Role",
|
|
26
|
+
grant: "Postgres::Grant",
|
|
27
|
+
defaultPrivileges: "Postgres::DefaultPrivileges",
|
|
28
|
+
};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The import IR as TypeScript: one `schema.ts` holding every imported object
|
|
3
|
+
* as a `schema`, `table`, `index`, `view`, `sequence`, `type`, `domain`,
|
|
4
|
+
* `extension`, `func`, `aggregate`, `collation`, `cast`, `eventTrigger` (and
|
|
5
|
+
* the rest of the tags) declaration, in dependency order, with the references between
|
|
6
|
+
* them interpolated, so the build orders them as the server did:
|
|
7
|
+
*
|
|
8
|
+
* - a qualified name of another imported object becomes `${export}`
|
|
9
|
+
* (`REFERENCES app.users(id)` is `REFERENCES ${users}(id)`, a type
|
|
10
|
+
* `app.order_status` is `${orderStatus}`);
|
|
11
|
+
* - a sequence named in a `regclass` literal becomes the reference
|
|
12
|
+
* (`nextval('app.invoice_seq'::regclass)` is `nextval(${invoiceSeq})`);
|
|
13
|
+
* - the schema part of the object's own name, wherever the name appears,
|
|
14
|
+
* becomes the schema's export (`CREATE TABLE ${appSchema}.users`);
|
|
15
|
+
* - an extension's `WITH SCHEMA`, a grant's `ON SCHEMA` and default
|
|
16
|
+
* privileges' `IN SCHEMA` name the schema's export;
|
|
17
|
+
* - a grant's object becomes the object's export (`GRANT SELECT ON TABLE
|
|
18
|
+
* ${orders} TO reader`), a policy's table its table's.
|
|
19
|
+
*
|
|
20
|
+
* Columns stay text, as they do for ClickHouse (#3236): which relation a bare
|
|
21
|
+
* column belongs to is the server's to resolve. One file, because the
|
|
22
|
+
* references cross schemas freely.
|
|
23
|
+
*/
|
|
24
|
+
import { relationKey } from "../../core/typed.js";
|
|
25
|
+
import { isTrivia, tokenizeText } from "../tokens.js";
|
|
26
|
+
import { identValue, parseStatements } from "../parser.js";
|
|
27
|
+
import { POSTGRES_ENTITY_TYPES } from "../entity-types.js";
|
|
28
|
+
const TAG = {
|
|
29
|
+
[POSTGRES_ENTITY_TYPES.schema]: "schema",
|
|
30
|
+
[POSTGRES_ENTITY_TYPES.table]: "table",
|
|
31
|
+
[POSTGRES_ENTITY_TYPES.index]: "index",
|
|
32
|
+
[POSTGRES_ENTITY_TYPES.view]: "view",
|
|
33
|
+
[POSTGRES_ENTITY_TYPES.materializedView]: "view",
|
|
34
|
+
[POSTGRES_ENTITY_TYPES.sequence]: "sequence",
|
|
35
|
+
[POSTGRES_ENTITY_TYPES.enum]: "type",
|
|
36
|
+
[POSTGRES_ENTITY_TYPES.composite]: "type",
|
|
37
|
+
[POSTGRES_ENTITY_TYPES.range]: "type",
|
|
38
|
+
[POSTGRES_ENTITY_TYPES.domain]: "domain",
|
|
39
|
+
[POSTGRES_ENTITY_TYPES.aggregate]: "aggregate",
|
|
40
|
+
[POSTGRES_ENTITY_TYPES.collation]: "collation",
|
|
41
|
+
[POSTGRES_ENTITY_TYPES.cast]: "cast",
|
|
42
|
+
[POSTGRES_ENTITY_TYPES.eventTrigger]: "eventTrigger",
|
|
43
|
+
[POSTGRES_ENTITY_TYPES.extension]: "extension",
|
|
44
|
+
[POSTGRES_ENTITY_TYPES.function]: "func",
|
|
45
|
+
[POSTGRES_ENTITY_TYPES.procedure]: "procedure",
|
|
46
|
+
[POSTGRES_ENTITY_TYPES.trigger]: "trigger",
|
|
47
|
+
[POSTGRES_ENTITY_TYPES.policy]: "policy",
|
|
48
|
+
[POSTGRES_ENTITY_TYPES.role]: "role",
|
|
49
|
+
[POSTGRES_ENTITY_TYPES.grant]: "grant",
|
|
50
|
+
[POSTGRES_ENTITY_TYPES.defaultPrivileges]: "grant",
|
|
51
|
+
};
|
|
52
|
+
/** The kinds no other object's DDL names. */
|
|
53
|
+
const UNREFERENCED = new Set([
|
|
54
|
+
POSTGRES_ENTITY_TYPES.index,
|
|
55
|
+
POSTGRES_ENTITY_TYPES.trigger,
|
|
56
|
+
POSTGRES_ENTITY_TYPES.policy,
|
|
57
|
+
POSTGRES_ENTITY_TYPES.grant,
|
|
58
|
+
POSTGRES_ENTITY_TYPES.defaultPrivileges,
|
|
59
|
+
POSTGRES_ENTITY_TYPES.role,
|
|
60
|
+
POSTGRES_ENTITY_TYPES.extension,
|
|
61
|
+
POSTGRES_ENTITY_TYPES.cast,
|
|
62
|
+
POSTGRES_ENTITY_TYPES.eventTrigger,
|
|
63
|
+
]);
|
|
64
|
+
/** The tag each entity type is declared with (`table`, `func`, ...). */
|
|
65
|
+
export const POSTGRES_TAG_OF = TAG;
|
|
66
|
+
/** Text that sits inside a template literal: a backquote and `${` escaped. */
|
|
67
|
+
const templateSafe = (s) => s.replace(/\\(?=`|\$\{)/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
68
|
+
const isName = (t) => t !== undefined && (t.kind === "ident" || t.kind === "qident");
|
|
69
|
+
/** `'app.invoice_seq'` as the qualified name it holds, or the bare name when unqualified. */
|
|
70
|
+
function regclassName(t) {
|
|
71
|
+
if (t.kind !== "string" || !/^'.*'$/s.test(t.text))
|
|
72
|
+
return undefined;
|
|
73
|
+
const inner = t.text.slice(1, -1).replace(/''/g, "'");
|
|
74
|
+
const pieces = inner.match(/"(?:[^"]|"")*"|[^.]+/g)?.map((p) => identValue(p)) ?? [];
|
|
75
|
+
return pieces.length === 2 ? `${pieces[0]}.${pieces[1]}` : pieces.length === 1 ? pieces[0] : undefined;
|
|
76
|
+
}
|
|
77
|
+
/** Words after which a bare name is a relation: `FROM t`, `JOIN t`, `REFERENCES t`. */
|
|
78
|
+
const RELATION_WORDS = new Set(["FROM", "JOIN", "REFERENCES"]);
|
|
79
|
+
/**
|
|
80
|
+
* The tokens where a bare (unqualified) name is another object's: a column's
|
|
81
|
+
* or a domain's type, the table of an index, trigger or policy, a trigger's
|
|
82
|
+
* function, a grant's objects. A `FROM`, `JOIN` or `REFERENCES` before a
|
|
83
|
+
* name is read off the tokens directly.
|
|
84
|
+
*/
|
|
85
|
+
function bareSpots(tokens) {
|
|
86
|
+
const out = new Set();
|
|
87
|
+
let nodes;
|
|
88
|
+
try {
|
|
89
|
+
nodes = parseStatements(tokens);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
const first = (from, to) => {
|
|
95
|
+
for (let i = from; i < to; i++)
|
|
96
|
+
if (!isTrivia(tokens[i]))
|
|
97
|
+
return out.add(i);
|
|
98
|
+
return out;
|
|
99
|
+
};
|
|
100
|
+
const name = (n) => n && n.span.to - n.span.from > 0 && first(n.span.from, n.span.to);
|
|
101
|
+
for (const node of nodes) {
|
|
102
|
+
if (node.statement === "table")
|
|
103
|
+
for (const c of node.columns)
|
|
104
|
+
if (c.type)
|
|
105
|
+
first(c.type.from, c.type.to);
|
|
106
|
+
if (node.statement === "domain")
|
|
107
|
+
first(node.type.from, node.type.to);
|
|
108
|
+
if (node.statement === "composite")
|
|
109
|
+
for (const a of node.attributes)
|
|
110
|
+
first(a.type.from, a.type.to);
|
|
111
|
+
if (node.statement === "cast") {
|
|
112
|
+
first(node.source.from, node.source.to);
|
|
113
|
+
first(node.target.from, node.target.to);
|
|
114
|
+
name(node.function);
|
|
115
|
+
}
|
|
116
|
+
if (node.statement === "eventTrigger")
|
|
117
|
+
name(node.function);
|
|
118
|
+
if (node.statement === "index" || node.statement === "policy")
|
|
119
|
+
name(node.table);
|
|
120
|
+
if (node.statement === "trigger") {
|
|
121
|
+
name(node.table);
|
|
122
|
+
name(node.function);
|
|
123
|
+
}
|
|
124
|
+
if (node.statement === "grant")
|
|
125
|
+
for (const o of node.objects)
|
|
126
|
+
name(o.name);
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
function templateBody(item, byQualified, schemaExport) {
|
|
131
|
+
const tokens = tokenizeText(item.ddl, 0);
|
|
132
|
+
const sig = [];
|
|
133
|
+
tokens.forEach((t, i) => {
|
|
134
|
+
if (!isTrivia(t))
|
|
135
|
+
sig.push(i);
|
|
136
|
+
});
|
|
137
|
+
const spots = bareSpots(tokens);
|
|
138
|
+
const replace = new Map();
|
|
139
|
+
const isSelf = (target) => target === item || (target.exportName === item.exportName && target.type === item.type);
|
|
140
|
+
for (let k = 0; k < sig.length; k++) {
|
|
141
|
+
const a = tokens[sig[k]];
|
|
142
|
+
const prev = tokens[sig[k - 1] ?? -1];
|
|
143
|
+
if (prev?.kind === "punct" && prev.text === ".")
|
|
144
|
+
continue;
|
|
145
|
+
// A regclass literal: nextval('app.seq'::regclass) or 'app.t'::regclass.
|
|
146
|
+
if (a.kind === "string" && tokens[sig[k + 1] ?? -1]?.text === "::" && tokens[sig[k + 2] ?? -1]?.text.toLowerCase() === "regclass") {
|
|
147
|
+
const held = regclassName(a) ?? "";
|
|
148
|
+
// An unqualified name is the object in `public`, as the server's own printing has it, else the unqualified object of that name.
|
|
149
|
+
const target = (!held.includes(".") ? byQualified.get(`public.${held}`) : undefined) ?? byQualified.get(held);
|
|
150
|
+
if (target && !isSelf(target)) {
|
|
151
|
+
const fn = tokens[sig[k - 2] ?? -1];
|
|
152
|
+
const inCall = prev?.text === "(" && fn?.kind === "ident" && /^(nextval|currval|setval)$/i.test(fn.text);
|
|
153
|
+
replace.set(sig[k], { to: inCall ? sig[k + 2] + 1 : sig[k] + 1, ref: target.exportName });
|
|
154
|
+
k += inCall ? 2 : 0;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const dot = tokens[sig[k + 1] ?? -1];
|
|
159
|
+
const b = tokens[sig[k + 2] ?? -1];
|
|
160
|
+
if (!isName(a) || dot?.kind !== "punct" || dot.text !== "." || !isName(b)) {
|
|
161
|
+
// An extension's `WITH SCHEMA app`, a grant's `ON SCHEMA app`, default privileges' `IN SCHEMA app`.
|
|
162
|
+
const bySchemaWord = item.type === POSTGRES_ENTITY_TYPES.extension || item.type === POSTGRES_ENTITY_TYPES.grant || item.type === POSTGRES_ENTITY_TYPES.defaultPrivileges;
|
|
163
|
+
if (bySchemaWord && isName(a) && prev?.kind === "ident" && prev.text.toUpperCase() === "SCHEMA") {
|
|
164
|
+
const s = schemaExport.get(identValue(a));
|
|
165
|
+
if (s)
|
|
166
|
+
replace.set(sig[k], { to: sig[k] + 1, ref: s });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
// A bare name where an object is named: `REFERENCES users`, `ON users`, a column of type `status`.
|
|
170
|
+
const bare = isName(a) && (spots.has(sig[k]) || (prev?.kind === "ident" && RELATION_WORDS.has(prev.text.toUpperCase())));
|
|
171
|
+
const target = bare ? byQualified.get(identValue(a)) : undefined;
|
|
172
|
+
if (target && !isSelf(target))
|
|
173
|
+
replace.set(sig[k], { to: sig[k] + 1, ref: target.exportName });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const schema = identValue(a);
|
|
177
|
+
const name = identValue(b);
|
|
178
|
+
const self = schema === item.schema && name === item.name;
|
|
179
|
+
const target = byQualified.get(`${schema}.${name}`);
|
|
180
|
+
if (!self && target && !isSelf(target)) {
|
|
181
|
+
replace.set(sig[k], { to: sig[k + 2] + 1, ref: target.exportName });
|
|
182
|
+
k += 2;
|
|
183
|
+
}
|
|
184
|
+
else if (self && schemaExport.has(schema)) {
|
|
185
|
+
replace.set(sig[k], { to: sig[k] + 1, ref: schemaExport.get(schema) });
|
|
186
|
+
k += 2;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const parts = [""];
|
|
190
|
+
const refs = [];
|
|
191
|
+
for (let i = 0; i < tokens.length;) {
|
|
192
|
+
const r = replace.get(i);
|
|
193
|
+
if (r) {
|
|
194
|
+
refs.push(r.ref);
|
|
195
|
+
parts.push("");
|
|
196
|
+
i = r.to;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
parts[parts.length - 1] += templateSafe(tokens[i].text);
|
|
200
|
+
i++;
|
|
201
|
+
}
|
|
202
|
+
return { parts, refs };
|
|
203
|
+
}
|
|
204
|
+
/** A template body as the text between the backquotes. */
|
|
205
|
+
export const bodyText = (body) => body.parts.map((p, i) => (i === 0 ? p : `\${${body.refs[i - 1]}}${p}`)).join("");
|
|
206
|
+
/**
|
|
207
|
+
* The template of each object in `items`, with the references to `targets`
|
|
208
|
+
* (the items themselves, and any object declared elsewhere that they may
|
|
209
|
+
* name) interpolated, and the items in the order a file declares them: each
|
|
210
|
+
* after what it references.
|
|
211
|
+
*
|
|
212
|
+
* A name is a reference where it is qualified (`app.users`), or bare where an
|
|
213
|
+
* object is named (`REFERENCES users`, `FROM users`, an index's table, a
|
|
214
|
+
* column's type) and an unqualified object has that name.
|
|
215
|
+
*/
|
|
216
|
+
export function postgresTemplates(items, targets = items) {
|
|
217
|
+
const byQualified = new Map();
|
|
218
|
+
const schemaExport = new Map();
|
|
219
|
+
for (const it of targets) {
|
|
220
|
+
if (it.type === POSTGRES_ENTITY_TYPES.schema)
|
|
221
|
+
schemaExport.set(it.name, it.exportName);
|
|
222
|
+
// An index, a trigger, a cast or an event trigger is never referenced by name in another object's DDL.
|
|
223
|
+
else if (!UNREFERENCED.has(it.type)) {
|
|
224
|
+
byQualified.set(it.schema ? `${it.schema}.${it.name}` : it.name, it);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const bodies = new Map(items.map((it) => [it.exportName, templateBody(it, byQualified, schemaExport)]));
|
|
228
|
+
const order = [];
|
|
229
|
+
const done = new Set();
|
|
230
|
+
const visiting = new Set();
|
|
231
|
+
const byExport = new Map(items.map((it) => [it.exportName, it]));
|
|
232
|
+
const visit = (it) => {
|
|
233
|
+
if (done.has(it.exportName) || visiting.has(it.exportName))
|
|
234
|
+
return;
|
|
235
|
+
visiting.add(it.exportName);
|
|
236
|
+
for (const ref of bodies.get(it.exportName).refs) {
|
|
237
|
+
const dep = byExport.get(ref);
|
|
238
|
+
if (dep)
|
|
239
|
+
visit(dep);
|
|
240
|
+
}
|
|
241
|
+
visiting.delete(it.exportName);
|
|
242
|
+
done.add(it.exportName);
|
|
243
|
+
order.push(it);
|
|
244
|
+
};
|
|
245
|
+
for (const it of items)
|
|
246
|
+
visit(it);
|
|
247
|
+
return { order, bodies };
|
|
248
|
+
}
|
|
249
|
+
const indent = (body) => body
|
|
250
|
+
.split("\n")
|
|
251
|
+
.map((l, i) => (i === 0 || l.length === 0 ? l : ` ${l}`))
|
|
252
|
+
.join("\n");
|
|
253
|
+
const RELATIONS = new Set([POSTGRES_ENTITY_TYPES.table, POSTGRES_ENTITY_TYPES.view, POSTGRES_ENTITY_TYPES.materializedView]);
|
|
254
|
+
/** A table's or view's qualified name as its tag's type argument (#3751), so its columns are typed once `chant sql types` has run. */
|
|
255
|
+
const typeArgument = (it) => (RELATIONS.has(it.type) ? `<${JSON.stringify(relationKey(it))}>` : "");
|
|
256
|
+
export class PostgresGenerator {
|
|
257
|
+
constructor(header) {
|
|
258
|
+
this.header = header;
|
|
259
|
+
this.ownsLayout = true;
|
|
260
|
+
}
|
|
261
|
+
generate(ir) {
|
|
262
|
+
const items = ir.resources
|
|
263
|
+
.filter((r) => TAG[r.type])
|
|
264
|
+
.map((r) => ({
|
|
265
|
+
exportName: r.logicalId,
|
|
266
|
+
type: r.type,
|
|
267
|
+
schema: r.properties.schema,
|
|
268
|
+
name: String(r.properties.name),
|
|
269
|
+
ddl: String(r.properties.ddl),
|
|
270
|
+
}));
|
|
271
|
+
if (items.length === 0)
|
|
272
|
+
return [];
|
|
273
|
+
const { order, bodies } = postgresTemplates(items);
|
|
274
|
+
const tags = [...new Set(order.map((it) => TAG[it.type]))].sort();
|
|
275
|
+
const lines = [];
|
|
276
|
+
if (this.header)
|
|
277
|
+
lines.push(...this.header.split("\n").map((l) => `// ${l}`), "");
|
|
278
|
+
lines.push(`import { ${tags.join(", ")} } from "@intentius/chant-lexicon-sql/postgres";`, "");
|
|
279
|
+
for (const it of order)
|
|
280
|
+
lines.push(`export const ${it.exportName} = ${TAG[it.type]}${typeArgument(it)}\`\n ${indent(bodyText(bodies.get(it.exportName)))}\`;`, "");
|
|
281
|
+
return [{ path: "schema.ts", content: `${lines.join("\n").trimEnd()}\n` }];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres objects into the import IR: one resource per object, the
|
|
3
|
+
* statements that create it as the server prints them (`../live/catalog.ts`),
|
|
4
|
+
* and an export name.
|
|
5
|
+
*/
|
|
6
|
+
import { POSTGRES_ENTITY_TYPES } from "../entity-types.js";
|
|
7
|
+
const JS_RESERVED = new Set("break case catch class const continue debugger default delete do else enum export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with yield let static implements interface package private protected public await".split(" "));
|
|
8
|
+
function camel(name) {
|
|
9
|
+
const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
10
|
+
if (words.length === 0)
|
|
11
|
+
return "object";
|
|
12
|
+
const s = words.map((w, i) => (i === 0 ? w[0].toLowerCase() + w.slice(1) : w[0].toUpperCase() + w.slice(1))).join("");
|
|
13
|
+
return /^[0-9]/.test(s) ? `o${s}` : s;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Export names: an object's name in camel case, prefixed with its schema when
|
|
17
|
+
* two schemas hold the same name; a schema's name with `Schema` after it and
|
|
18
|
+
* an extension's with `Extension`, so `app` the schema and `app` a table do
|
|
19
|
+
* not collide.
|
|
20
|
+
*/
|
|
21
|
+
export function exportNames(objects) {
|
|
22
|
+
const count = new Map();
|
|
23
|
+
for (const o of objects)
|
|
24
|
+
if (o.schema)
|
|
25
|
+
count.set(o.name, (count.get(o.name) ?? 0) + 1);
|
|
26
|
+
const used = new Set();
|
|
27
|
+
return objects.map((o) => {
|
|
28
|
+
let base = o.type === POSTGRES_ENTITY_TYPES.schema
|
|
29
|
+
? `${camel(o.name)}Schema`
|
|
30
|
+
: o.type === POSTGRES_ENTITY_TYPES.extension
|
|
31
|
+
? `${camel(o.name)}Extension`
|
|
32
|
+
: (count.get(o.name) ?? 0) > 1 && o.schema
|
|
33
|
+
? camel(`${o.schema}_${o.name}`)
|
|
34
|
+
: camel(o.name);
|
|
35
|
+
if (JS_RESERVED.has(base))
|
|
36
|
+
base = `${base}Object`;
|
|
37
|
+
let name = base;
|
|
38
|
+
for (let i = 2; used.has(name); i++)
|
|
39
|
+
name = `${base}${i}`;
|
|
40
|
+
used.add(name);
|
|
41
|
+
return name;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/** The IR for a set of objects. */
|
|
45
|
+
export function objectsToIR(objects, warnings = []) {
|
|
46
|
+
const names = exportNames(objects);
|
|
47
|
+
const resources = objects.map((o, i) => ({
|
|
48
|
+
logicalId: names[i],
|
|
49
|
+
type: o.type,
|
|
50
|
+
properties: { ...(o.schema ? { schema: o.schema } : {}), name: o.name, ddl: o.ddl },
|
|
51
|
+
}));
|
|
52
|
+
return { resources, parameters: [], ...(warnings.length ? { warnings } : {}) };
|
|
53
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live export: `chant import --from <env>` writes a Postgres server's schema as
|
|
3
|
+
* declarations, each object as the statements the catalog's own printers
|
|
4
|
+
* give for it (`../live/catalog.ts`), the canonical form.
|
|
5
|
+
*
|
|
6
|
+
* - `selector.type` is an entity type (`Postgres::Table`); `selector.name` is
|
|
7
|
+
* `name` or `schema.name`.
|
|
8
|
+
* - `owned` keeps the objects whose comment carries chant's ownership marker.
|
|
9
|
+
* - Another tool's objects (an ORM's revision table, and its sequence and
|
|
10
|
+
* indexes) are left out with a warning naming the tool: they are its, and
|
|
11
|
+
* a declaration of them would have chant fight it.
|
|
12
|
+
* - `verbatim` changes nothing yet: the printers already leave out what the
|
|
13
|
+
* server would add by itself.
|
|
14
|
+
* - Where the profile manages access (`sql.profiles.<env>.access`), the
|
|
15
|
+
* policies and each table's row-level security come with the objects, and
|
|
16
|
+
* the privileges on them and the default privileges are written as `grant`
|
|
17
|
+
* declarations: the GRANT and REVOKE statements that take a new object's
|
|
18
|
+
* privileges to what the server holds. Roles are the environment's and are
|
|
19
|
+
* not imported.
|
|
20
|
+
* - An object of a kind no declaration holds (an aggregate, a cast, a
|
|
21
|
+
* composite type, an event trigger, ...; `../live/undeclarable.ts`) fails
|
|
22
|
+
* the import, which names every one of them (#3911). `skipUndeclarable`
|
|
23
|
+
* (`--skip-undeclarable`) imports the rest and lists them as warnings. A
|
|
24
|
+
* selector or `owned` import asks for objects chant can hold, so it does
|
|
25
|
+
* not look.
|
|
26
|
+
*/
|
|
27
|
+
import { bindPostgres } from "../live/bind.js";
|
|
28
|
+
import { markProviderOwned, readLiveSchema } from "../live/catalog.js";
|
|
29
|
+
import { objectsToIR } from "./ir.js";
|
|
30
|
+
import { isChantManaged } from "../../core/ownership.js";
|
|
31
|
+
import { importedAccess } from "../access/import.js";
|
|
32
|
+
import { readUndeclarable } from "../live/undeclarable.js";
|
|
33
|
+
import { describeUndeclarable, refuseUndeclarable } from "../../core/undeclarable.js";
|
|
34
|
+
import { isProviderOwned, providerData } from "../providers/index.js";
|
|
35
|
+
export async function exportResources(options) {
|
|
36
|
+
const { target, client } = await bindPostgres(options);
|
|
37
|
+
let live;
|
|
38
|
+
let access = [];
|
|
39
|
+
// A selector or an owned import asks only for what chant can hold.
|
|
40
|
+
const checked = !options.selector && !options.owned;
|
|
41
|
+
let undeclarable = { objects: [], unread: [] };
|
|
42
|
+
try {
|
|
43
|
+
if (checked)
|
|
44
|
+
undeclarable = await readUndeclarable(client, { schemas: target.schemas });
|
|
45
|
+
live = markProviderOwned(await readLiveSchema(client, { schemas: target.schemas, access: target.access === true }), target.provider);
|
|
46
|
+
// Where the profile manages access, the privileges on what is imported, and the default privileges, as grant declarations.
|
|
47
|
+
if (target.access === true && !options.selector)
|
|
48
|
+
access = await importedAccess(client, live.filter((o) => !o.foreign && !o.unsupported && (!options.owned || isChantManaged(o.comment))), target.schemas);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await client.end();
|
|
52
|
+
}
|
|
53
|
+
const warnings = undeclarable.unread.map((u) => `the server was not checked for objects of one kind chant cannot declare, so some may be left out: ${u}`);
|
|
54
|
+
// A managed provider's own (in its schemas, or owned by its roles) is the provider's, as for the objects read below.
|
|
55
|
+
const provider = target.provider;
|
|
56
|
+
const providers = (o) => provider !== undefined &&
|
|
57
|
+
((o.schema !== undefined && isProviderOwned(provider, { kind: "schema", name: o.schema })) || (o.owner !== undefined && isProviderOwned(provider, { kind: "role", name: o.owner })));
|
|
58
|
+
for (const o of undeclarable.objects.filter(providers))
|
|
59
|
+
warnings.push(`${describeUndeclarable(o)} is kept by ${providerData(provider).label}; left out`);
|
|
60
|
+
refuseUndeclarable("postgres", undeclarable.objects.filter((o) => !providers(o)), options.skipUndeclarable, warnings);
|
|
61
|
+
const objects = [];
|
|
62
|
+
for (const o of live) {
|
|
63
|
+
if (options.owned && !isChantManaged(o.comment))
|
|
64
|
+
continue;
|
|
65
|
+
if (options.selector?.type && o.type !== options.selector.type)
|
|
66
|
+
continue;
|
|
67
|
+
const name = options.selector?.name;
|
|
68
|
+
if (name !== undefined && name !== o.name && name !== `${o.schema}.${o.name}`)
|
|
69
|
+
continue;
|
|
70
|
+
if (o.unsupported) {
|
|
71
|
+
// When checked, it is among the undeclarable objects above.
|
|
72
|
+
if (!checked)
|
|
73
|
+
warnings.push(`${o.schema ? `${o.schema}.` : ""}${o.name}${o.signature ?? ""} is left out: ${o.unsupported}`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (o.foreign) {
|
|
77
|
+
warnings.push(`${o.schema ? `${o.schema}.` : ""}${o.name} is kept by ${o.foreign}; left out, since declaring it would have chant change what that tool owns`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
objects.push({ type: o.type, ...(o.schema ? { schema: o.schema } : {}), name: o.name, ddl: o.statement });
|
|
81
|
+
}
|
|
82
|
+
return objectsToIR([...objects, ...access], warnings);
|
|
83
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres key words and their categories at the pinned server, as
|
|
3
|
+
* `pg_get_keywords()` reports them: what decides whether a bare word can be a
|
|
4
|
+
* name. The parser refuses an unquoted reserved word (`R`) or type/function
|
|
5
|
+
* name word (`T`) where a column, table or constraint name goes, as Postgres's
|
|
6
|
+
* `ColId` does, and quoting reads the same table.
|
|
7
|
+
*/
|
|
8
|
+
import { KEYWORDS as GENERATED_KEYWORDS } from "../generated/postgres.js";
|
|
9
|
+
/** Generated from `pg_get_keywords()` at the latest pinned major (`src/spec/postgres-catalog-18.snapshot.json`). */
|
|
10
|
+
const KEYWORDS = GENERATED_KEYWORDS;
|
|
11
|
+
/** A word's key word category, case-insensitively; undefined when it is not a key word. */
|
|
12
|
+
export function keywordCategory(word) {
|
|
13
|
+
const w = word.toLowerCase();
|
|
14
|
+
return Object.prototype.hasOwnProperty.call(KEYWORDS, w) ? KEYWORDS[w] : undefined;
|
|
15
|
+
}
|
|
16
|
+
/** Whether `word` is a key word of any category. */
|
|
17
|
+
export const isKeyword = (word) => keywordCategory(word) !== undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Whether `name` must be double-quoted to read back as itself: anything but a
|
|
20
|
+
* lower-case identifier, and every key word that is not unreserved (a
|
|
21
|
+
* `C` word such as `bigint` is a name only where a column name goes).
|
|
22
|
+
*/
|
|
23
|
+
export function needsQuotes(name) {
|
|
24
|
+
if (!/^[a-z_][a-z0-9_$]*$/.test(name))
|
|
25
|
+
return true;
|
|
26
|
+
const k = keywordCategory(name);
|
|
27
|
+
return k !== undefined && k !== "U";
|
|
28
|
+
}
|
|
29
|
+
/** A name as Postgres reads it back: bare when it can be, double-quoted otherwise. */
|
|
30
|
+
export function quoteIdent(name) {
|
|
31
|
+
return needsQuotes(name) ? `"${name.replace(/"/g, '""')}"` : name;
|
|
32
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Postgres server a chant environment reads: the config lookup and the
|
|
3
|
+
* binding, shared by observation, live export and the plan, so all of them
|
|
4
|
+
* read the same server.
|
|
5
|
+
*/
|
|
6
|
+
import { connectPostgres, DEFAULT_LOCK_TIMEOUT_MS, PostgresQueryError, withCatalogLockTimeout } from "./client.js";
|
|
7
|
+
import { credentialSource, TokenSourceError } from "../../token-source.js";
|
|
8
|
+
export const isUnresolvedTarget = (v) => "reason" in v;
|
|
9
|
+
/** Whether a URL names a Postgres server. */
|
|
10
|
+
export const isPostgresUrl = (url) => /^postgres(ql)?:\/\//i.test(url);
|
|
11
|
+
/** A URL with any password in it taken out, for a message. */
|
|
12
|
+
export const redactUrl = (url) => url.replace(/^(postgres(?:ql)?:\/\/[^:/@]+):[^@]*@/i, "$1:***@");
|
|
13
|
+
/**
|
|
14
|
+
* The server an environment reads. Pure: `config` and `env` are passed in.
|
|
15
|
+
*
|
|
16
|
+
* 1. `sql.profiles.<environment>` with a `postgres://` URL. A profile naming a
|
|
17
|
+
* credential variable that is not set is `no-credentials`.
|
|
18
|
+
* 2. `POSTGRES_URL`, with `POSTGRES_USER` and `POSTGRES_PASSWORD` when set.
|
|
19
|
+
* 3. Otherwise `no-binding`.
|
|
20
|
+
*/
|
|
21
|
+
export function resolvePostgresTarget(input) {
|
|
22
|
+
const env = input.env ?? process.env;
|
|
23
|
+
const profile = input.environment !== undefined ? input.config?.sql?.profiles?.[input.environment] : undefined;
|
|
24
|
+
const provider = (profile && isPostgresUrl(profile.url) ? profile.provider : undefined) ?? input.config?.sql?.provider;
|
|
25
|
+
const withProvider = (t) => (provider ? { ...t, provider } : t);
|
|
26
|
+
if (profile && isPostgresUrl(profile.url)) {
|
|
27
|
+
const source = `sql.profiles.${input.environment}`;
|
|
28
|
+
const endpoint = { url: profile.url };
|
|
29
|
+
if (profile.user) {
|
|
30
|
+
const value = env[profile.user.env];
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return { reason: "no-credentials", detail: `${source}.user names ${profile.user.env}, which is not set` };
|
|
33
|
+
endpoint.user = value;
|
|
34
|
+
}
|
|
35
|
+
const password = profile.password;
|
|
36
|
+
if (password && "env" in password) {
|
|
37
|
+
const value = env[password.env];
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
return { reason: "no-credentials", detail: `${source}.password names ${password.env}, which is not set` };
|
|
40
|
+
endpoint.password = value;
|
|
41
|
+
}
|
|
42
|
+
else if (password) {
|
|
43
|
+
if (password.token === "rds-iam" && endpoint.user === undefined) {
|
|
44
|
+
return { reason: "no-credentials", detail: `${source}.password is an rds-iam token, which is minted for one user, and ${source}.user is not set` };
|
|
45
|
+
}
|
|
46
|
+
endpoint.token = credentialSource(password, { url: profile.url, ...(endpoint.user !== undefined ? { user: endpoint.user } : {}), env, source: `${source}.password` });
|
|
47
|
+
}
|
|
48
|
+
const timeouts = Object.fromEntries(["lockTimeoutMs", "statementTimeoutMs", "scanTimeoutMs"].filter((k) => profile[k] !== undefined).map((k) => [k, profile[k]]));
|
|
49
|
+
return withProvider({
|
|
50
|
+
endpoint,
|
|
51
|
+
source,
|
|
52
|
+
...(profile.schemas ? { schemas: profile.schemas } : {}),
|
|
53
|
+
defaultSchema: profile.defaultSchema ?? "public",
|
|
54
|
+
...(Object.keys(timeouts).length > 0 ? { timeouts } : {}),
|
|
55
|
+
...(profile.access === true ? { access: true } : {}),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const url = env.POSTGRES_URL;
|
|
59
|
+
if (!url) {
|
|
60
|
+
const where = input.environment !== undefined ? `sql.profiles.${input.environment} names no postgres:// server and ` : "";
|
|
61
|
+
return { reason: "no-binding", detail: `${where}POSTGRES_URL is not set, so there is no Postgres server to read` };
|
|
62
|
+
}
|
|
63
|
+
return withProvider({
|
|
64
|
+
endpoint: {
|
|
65
|
+
url,
|
|
66
|
+
...(env.POSTGRES_USER !== undefined ? { user: env.POSTGRES_USER } : {}),
|
|
67
|
+
...(env.POSTGRES_PASSWORD !== undefined ? { password: env.POSTGRES_PASSWORD } : {}),
|
|
68
|
+
},
|
|
69
|
+
source: "env POSTGRES_URL",
|
|
70
|
+
defaultSchema: "public",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** A target that could not be resolved. */
|
|
74
|
+
export class PostgresBindingError extends Error {
|
|
75
|
+
constructor(unresolved) {
|
|
76
|
+
super(unresolved.detail);
|
|
77
|
+
this.unresolved = unresolved;
|
|
78
|
+
this.name = "PostgresBindingError";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export async function loadSqlConfig(cwd) {
|
|
82
|
+
try {
|
|
83
|
+
const { loadChantConfig } = await import("@intentius/chant/config");
|
|
84
|
+
return (await loadChantConfig(cwd)).config;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// No project config: the environment variables still bind.
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Resolve the environment's server. Throws {@link PostgresBindingError} when there is none. */
|
|
92
|
+
export async function resolveBoundTarget(options = {}) {
|
|
93
|
+
const config = options.config ?? (await loadSqlConfig(options.cwd ?? process.cwd()));
|
|
94
|
+
const target = resolvePostgresTarget({ environment: options.environment, config, env: options.env });
|
|
95
|
+
if (isUnresolvedTarget(target))
|
|
96
|
+
throw new PostgresBindingError(target);
|
|
97
|
+
return target;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve the environment's server and connect to it. The caller ends the client.
|
|
101
|
+
*
|
|
102
|
+
* The session reads under the profile's `lockTimeoutMs` (the applier's 5000 ms
|
|
103
|
+
* otherwise): a catalog read blocked behind ACCESS EXCLUSIVE fails with the
|
|
104
|
+
* relation and the pid that holds it, instead of waiting for as long as the
|
|
105
|
+
* lock is held.
|
|
106
|
+
*/
|
|
107
|
+
export async function bindPostgres(options = {}) {
|
|
108
|
+
const target = await resolveBoundTarget(options);
|
|
109
|
+
const raw = await (options.connect ?? connectPostgres)(target.endpoint);
|
|
110
|
+
try {
|
|
111
|
+
return { target, client: await withCatalogLockTimeout(raw, target.timeouts?.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS) };
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
await raw.end();
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** SQLSTATEs that mean the server refused who we are: invalid_authorization_specification, invalid_password, insufficient_privilege. */
|
|
119
|
+
const CREDENTIAL_STATES = new Set(["28000", "28P01", "42501"]);
|
|
120
|
+
/** What a failed bind or read means, in the observation vocabulary. */
|
|
121
|
+
export function classifyPostgresFailure(err) {
|
|
122
|
+
if (err instanceof PostgresBindingError)
|
|
123
|
+
return { reason: err.unresolved.reason, detail: err.unresolved.detail };
|
|
124
|
+
if (err instanceof TokenSourceError)
|
|
125
|
+
return { reason: "no-credentials", detail: err.message };
|
|
126
|
+
if (err instanceof PostgresQueryError && err.code && CREDENTIAL_STATES.has(err.code)) {
|
|
127
|
+
return { reason: "no-credentials", detail: `${err.message} (the server refused the credentials, SQLSTATE ${err.code})` };
|
|
128
|
+
}
|
|
129
|
+
return { reason: "read-failed", detail: err instanceof Error ? err.message.split("\n")[0] : String(err) };
|
|
130
|
+
}
|