Skip to content

Commit 485a33d

Browse files
fuziontechclaude
andauthored
feat: accept an org's Trino catalog name as a logical catalog alias (#1177)
* feat: accept an org's Trino catalog name as a logical catalog alias A Duckgres session may now connect with `database=org_<database_name>` — the same catalog name the org has on Trino — and get the physical DuckLake catalog under that name. current_database(), pg_database, information_schema, three-part references, and `USE` all answer to it. The point is SQLMesh: it sees ONE catalog name on both the Duckgres and Trino engines, so moving a project between engines needs no state rewrite. PR #651's invariant is preserved. The startup `database` is still never used to find, select, or route to an org. Identity stays SNI-only, and the alias is validated AGAINST the org SNI has already resolved — it is compared to that org's own catalog name, never used as a lookup key. A sibling tenant's catalog name is just another unrecognized string and fails closed, exactly as today. Sessions connecting with "ducklake" or nothing are unchanged; the alias is opt-in per connection. The name derivation moves to configstore (untagged) so every build has it; provisioner.TrinoCatalogName now delegates to that one definition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCY5Jf2BQPCVKJTZU1TpEe * test(e2e): assert the logical catalog alias against mw-dev logical_catalog_alias connects the cnpg tenant with its own Trino catalog name and asserts the alias renames without redirecting: current_database() and pg_database report it, a three-part reference and `USE <alias>` reach the real catalog, and a session connected the ordinary way sees the same row. It also asserts the security half — a sibling tenant's catalog name and an arbitrary name both get 3D000. Docs: CLAUDE.md gains the alias contract and the PR #651 invariant that governs it; the harness path references are corrected to the directory that exists (tests/mw-dev/e2e/, not tests/e2e-mw-dev/). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCY5Jf2BQPCVKJTZU1TpEe * refactor: drop two unreachable guards in the alias checks `requestedCatalog != ""` cannot be false where it sits — an empty startup database already set CatalogValid — and `c.database != ""` is subsumed by the EqualFold against a non-empty USE target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCY5Jf2BQPCVKJTZU1TpEe * fix: restore the empty-database guard in namesDuckLakeCatalog dcb043d dropped `c.database != ""` on the claim that it was "subsumed by the EqualFold against a non-empty USE target". That claim was wrong: the USE target is only checked for emptiness BEFORE quote-stripping, so `USE ""` passes that check as the two-character `""` and then unquotes to "". A session whose database is unset then matched its own empty name, and invalid SQL was silently rewritten into `USE ducklake.main` instead of passing through to DuckDB and erroring. No cross-tenant exposure — it resolves to the catalog the session already executes against — but an invalid statement must not silently succeed. The other guard that commit dropped (`requestedCatalog != ""` in store.go) is genuinely unreachable and stays dropped. TestRewriteDirectQueryEmptyQuotedUseIsNotAnAlias pins it at the rewrite entry point, plus an `USE ""` case in the alias table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCY5Jf2BQPCVKJTZU1TpEe * fix(e2e): send USE as its own message in the logical-alias assertion The e2e assertion batched `USE <alias>; SELECT ...` into one psql -c, which duckgres cannot split. handleQuery splits a multi-statement simple query only when pg_query parses it (conn.go: `parseErr == nil && len(tree.Stmts) > 1`), and `USE` is not PostgreSQL syntax — so the batch reached rewriteDirectQuery whole, its USE target was `<alias>; SELECT ...` (matching no catalog name, so correctly left alone), and DuckDB split it and failed the bare USE. The product code was right; the assertion was written in a shape the simple query protocol does not support here. `USE ducklake; SELECT ...` fails the same way on main, so this is pre-existing and not alias-specific. pg_script feeds a script on stdin, where psql sends each statement as its own simple-query message on ONE session. Verified against a local standalone server: state set by the first statement is visible to the third, on one pid. Tests: TestLogicalCatalogAliasThroughConnectionSetup drives the alias through the REAL setup path — NewClientConn plus the exported setters control.go calls, in order, including the post-worker-switch replay — for both the simple and the extended query composition. That closes the gap that let this reach CI: the old tests hand-built a clientConn and never exercised how the session fields get populated. TestUseStatementIsNeverSplitOutOfASimpleQueryBatch pins the batching limitation so the next harness author does not rediscover it, and fails loudly if pg_query ever learns to parse USE. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCY5Jf2BQPCVKJTZU1TpEe * docs: record the catalog-qualified SET search_path gap `SET search_path = '<alias>.main'` is not rewritten and fails on the worker, while the physical `'ducklake.main'` works. The catalog name sits in a string literal rather than a RangeVar, so LogicalCatalogTransform has nothing to match and the USE pass does not look at SET. Deliberately left alone: SQLMesh selects a catalog with `USE <catalog>` as its own statement, and touches search_path only in dbt code it marks unsupported. Also record in the harness that project_reader_isolation already issues its USE as a separate message (psql -c/-c) and is not a latent failure, and that the retry case list now has four copies worth folding together later. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCY5Jf2BQPCVKJTZU1TpEe --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 72f5f45 commit 485a33d

16 files changed

Lines changed: 752 additions & 62 deletions

CLAUDE.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ The project uses [just](https://github.com/casey/just) as a command runner. Run
180180

181181
**Every feature, behavior change, bugfix, AND refactor that affects runtime or
182182
cluster behavior MUST ship with a solid end-to-end test case in
183-
`tests/e2e-mw-dev/` (`harness.sh`).** This is not just for new features — any
183+
`tests/mw-dev/e2e/` (`harness.sh`).** This is not just for new features — any
184184
change to how the system behaves at runtime (new capability, changed semantics,
185185
a fixed bug, a new config knob, an activation/routing/teardown tweak) extends or
186186
adds a harness assertion in the same PR. Refactors count too: when you move or
@@ -202,11 +202,11 @@ Three test lanes worth knowing about, in increasing order of blast radius:
202202

203203
- **Unit / package tests** (`go test ./...`): in-process, no external deps. Where most coverage lives. Includes `tests/manifests/` (static-manifest artifact asserts for `k8s/rbac.yaml` + `k8s/networkpolicy.yaml`).
204204
- **`tests/integration/`** (`just test-integration`): spins up the standalone server binary against a real MinIO + Postgres metadata store via docker compose. Covers wire protocol, DuckLake on real S3-compatible storage, transpilation against a live server.
205-
- **`tests/e2e-mw-dev/`** (per-PR GitHub workflow `e2e-mw-dev.yml`): the full multi-tenant activation pipeline against the **real posthog-mw-dev EKS cluster** — real Cilium, real Crossplane ducklings, real cnpg-shard + external-RDS metadata, real AWS S3. A shell harness (`harness.sh`) runs as an in-cluster Job per PR; `run.sh` orchestrates deploy/test/teardown/e2e-cleanup. **Replaces the retired kind suite** (`tests/k8s/`) — that suite's `k8s-integration-tests` CI job and its Go tests are gone; the supporting `k8s/` scripts/manifests + Dockerfiles are kept for now. See `tests/e2e-mw-dev/README.md`.
205+
- **`tests/mw-dev/e2e/`** (per-PR GitHub workflow `e2e-mw-dev.yml`): the full multi-tenant activation pipeline against the **real posthog-mw-dev EKS cluster** — real Cilium, real Crossplane ducklings, real cnpg-shard + external-RDS metadata, real AWS S3. A shell harness (`harness.sh`) runs as an in-cluster Job per PR; `run.sh` orchestrates deploy/test/teardown/e2e-cleanup. **Replaces the retired kind suite** (`tests/k8s/`) — that suite's `k8s-integration-tests` CI job and its Go tests are gone; the supporting `k8s/` scripts/manifests + Dockerfiles are kept for now. See `tests/mw-dev/README.md`.
206206

207207
### When code changes obligate test changes
208208

209-
`tests/e2e-mw-dev/` is the only place we exercise the full activation pipeline (control plane → STS broker → worker pod → DuckDB → ATTACH against real cloud storage). If your change touches any of the following, treat updating the harness as part of the change, not a follow-up:
209+
`tests/mw-dev/e2e/` is the only place we exercise the full activation pipeline (control plane → STS broker → worker pod → DuckDB → ATTACH against real cloud storage). If your change touches any of the following, treat updating the harness as part of the change, not a follow-up:
210210

211211
- `controlplane/shared_worker_activator.go`, `controlplane/sts_broker.go`, anything in the activation payload shape (`TenantActivationPayload`, `server.DuckLakeConfig`)
212212
- `server/server.go::AttachDeltaCatalog`, `server.attachDuckLake*`, `server.refresh*Secret`
@@ -1716,6 +1716,39 @@ password/tenant/catalog changes never propagate.
17161716
the `ui/src/lib/trino.test.ts` derivations and
17171717
`tests/mw-dev/e2e/trino.sh`.
17181718

1719+
## Logical Catalog Alias (`org_<database_name>` as the startup `database`)
1720+
1721+
A pgwire session may select its catalog by the name the org has on Trino
1722+
(`configstore.TrinoCatalogName`, `org_<sanitized database_name>`) instead of
1723+
`ducklake`. The catalog is the same one either way: the alias only renames it
1724+
on the wire. This exists so SQLMesh sees ONE catalog name across the Duckgres
1725+
and Trino engines and migrating between them needs no state rewrite.
1726+
1727+
- **What the startup `database` may be**: `""`, `ducklake`, or the
1728+
SNI-resolved org's own catalog name. Everything else is 3D000, as before.
1729+
- **Identity is still SNI-only, and this must stay true.** The alias is
1730+
compared against the catalog name of the org the managed hostname ALREADY
1731+
resolved — it is never a key into `DatabaseOrg`, `Orgs`, or any other map,
1732+
so it can neither discover nor select an org. A sibling tenant's catalog
1733+
name is just an unrecognized string and fails closed. If a change here ever
1734+
looks up an org BY the database name, it has reintroduced exactly what PR
1735+
#651 removed. See the comment in `ResolvePostgresConnection`.
1736+
- **Physical vs. visible**: `EffectiveCatalog` stays `ducklake` and is what
1737+
every statement executes against; `LogicalCatalog` (and
1738+
`sessionMetadataResult.visibleCatalog`) is the name reported by
1739+
`current_database()`, `pg_database`, `information_schema`, and the logs.
1740+
`visibleCatalogName` is the one place that chooses between them.
1741+
- **SQL written against the alias**: the transpiler's `LogicalCatalogTransform`
1742+
rewrites `<alias>.public.t``ducklake.main.t` (fed by `clientConn.database`
1743+
in `newTranspiler`), and `rewriteDirectQuery` expands `USE <alias>` to
1744+
`ducklake.main`.
1745+
- Opt-in per connection: a session that connects with `ducklake` or nothing is
1746+
byte-for-byte unaffected.
1747+
- Touching any of this → update `controlplane/configstore/store_test.go`,
1748+
`controlplane/session_search_path_test.go`, `server/direct_query_rewrite_test.go`,
1749+
`server/logical_catalog_alias_test.go`, `server/session_database_metadata_test.go`,
1750+
and `logical_catalog_alias` in `tests/mw-dev/e2e/harness.sh`.
1751+
17191752
## TODO Reference
17201753

17211754
`TODO.md` is a lightweight backlog for ideas that do not yet have a better

cmd/cache-proxy/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ observed peer outcome (`present`, `in_flight`, `negative`, `timeout`,
278278
span for every probe. `org_id` is intentionally absent — the proxy has no
279279
per-request tenant identity.
280280

281-
> The cache proxy is not deployed in the `tests/e2e-mw-dev` environment
281+
> The cache proxy is not deployed in the `tests/mw-dev/e2e` environment
282282
> (`DUCKGRES_CACHE_ENABLED` is off there). Unit tests in
283283
> `cmd/cache-proxy/tracing_test.go` cover propagation behavior; validate the
284284
> complete trace in a cache-enabled dev deployment.

controlplane/admin/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ region.
337337
`dashboard_test.go` (TokenSet / break-glass login / cookie), `api_test.go` +
338338
`api_postgres_test.go` (CRUD), `models_api_test.go` (redaction). e2e: the
339339
`admin_*` / `impersonation_*` / `models_explorer_api` assertions in
340-
`tests/e2e-mw-dev/harness.sh`.
340+
`tests/mw-dev/e2e/harness.sh`.
341341

342342
**Frontend** (`ui/`, Vitest + Testing Library — `just ui-test`, CI job
343343
`ui-tests`): the dashboard's data-derivation logic has shipped wrong more than

controlplane/configstore/store.go

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,10 @@ type Snapshot struct {
110110
OrgUserAccess map[OrgUserKey]OrgUserAccessConfig
111111
}
112112

113-
// Selectable catalog names. The startup `database` param now names the catalog
114-
// a session defaults to rather than identifying the org — these are the only
115-
// non-empty values a client may request.
113+
// The physical catalog name. The startup `database` param now names the
114+
// catalog a session defaults to rather than identifying the org; a client may
115+
// request this, "", or its own org's logical alias (TrinoCatalogName of the
116+
// org's database_name) and nothing else.
116117
const (
117118
catalogDuckLake = "ducklake"
118119
)
@@ -133,11 +134,21 @@ type PostgresConnectionResolution struct {
133134
SNIAliasUsed bool
134135
// SNIResolved is true when the managed hostname resolved to a known org.
135136
SNIResolved bool
136-
// EffectiveCatalog is the catalog the session should default to, selected by
137-
// the startup `database` param: "" (use the attached default) or "ducklake".
137+
// EffectiveCatalog is the REAL catalog the session should default to,
138+
// selected by the startup `database` param: "" (use the attached default)
139+
// or "ducklake". A logical alias still resolves to "ducklake" here —
140+
// execution always targets the physical catalog.
138141
EffectiveCatalog string
142+
// LogicalCatalog is the client-visible name for that same catalog, set only
143+
// when the startup `database` matched the SNI-resolved org's own Trino
144+
// catalog name (`org_<database_name>`). Empty for "" and "ducklake", which
145+
// report the physical name. It renames the catalog on the PG wire
146+
// (current_database(), pg_database, information_schema) and in three-part
147+
// references; it never changes what the session executes against.
148+
LogicalCatalog string
139149
// CatalogValid is false when the requested `database` is not a selectable
140-
// catalog name (anything other than "" or "ducklake").
150+
// catalog name: anything other than "", "ducklake", or the SNI-resolved
151+
// org's own catalog name.
141152
CatalogValid bool
142153
// Valid is true when (OrgID, username, password) authenticated.
143154
Valid bool
@@ -565,10 +576,12 @@ func (cs *ConfigStore) ResolvePostgresConnection(startupDatabase, sniPrefix stri
565576
result := PostgresConnectionResolution{}
566577

567578
// The startup `database` param is now pure catalog selection, not identity.
568-
// Valid values: "" (use the attached default) or "ducklake". Anything else
569-
// fails closed — there is no logical-name masking, so an arbitrary name no
570-
// longer routes anywhere.
571-
switch strings.ToLower(strings.TrimSpace(startupDatabase)) {
579+
// Valid values: "" (use the attached default), "ducklake", or — resolved
580+
// further down, once SNI has named an org — that org's own Trino catalog
581+
// name. Anything else fails closed: there is no logical-name masking, so an
582+
// arbitrary name no longer routes anywhere.
583+
requestedCatalog := strings.ToLower(strings.TrimSpace(startupDatabase))
584+
switch requestedCatalog {
572585
case "":
573586
result.CatalogValid = true
574587
case catalogDuckLake:
@@ -588,7 +601,7 @@ func (cs *ConfigStore) ResolvePostgresConnection(startupDatabase, sniPrefix stri
588601
if !useManagedSNI {
589602
return result
590603
}
591-
orgID, _, aliasUsed := resolveSNIPrefixFromSnapshot(cs.snapshot, sniPrefix)
604+
orgID, databaseName, aliasUsed := resolveSNIPrefixFromSnapshot(cs.snapshot, sniPrefix)
592605
if orgID == "" {
593606
return result
594607
}
@@ -597,6 +610,24 @@ func (cs *ConfigStore) ResolvePostgresConnection(startupDatabase, sniPrefix stri
597610
result.SNIOrgID = orgID
598611
result.OrgID = orgID
599612

613+
// Logical catalog alias. A session may also name the catalog THIS org
614+
// already has on Trino (`org_<database_name>`) and get the same physical
615+
// DuckLake catalog under that name, so one engine-agnostic catalog name
616+
// works on both engines.
617+
//
618+
// Direction matters, and it is the whole of PR #651's invariant: the name
619+
// is compared against the catalog name derived from the org SNI has ALREADY
620+
// resolved. It is never a key into DatabaseOrg, Orgs, or any other map, so
621+
// it can neither discover nor select an org — a sibling tenant's catalog
622+
// name is just another unrecognized string here, and fails closed. Never
623+
// rewrite this as a lookup from name to org.
624+
if !result.CatalogValid && databaseName != "" &&
625+
requestedCatalog == TrinoCatalogName(databaseName) {
626+
result.EffectiveCatalog = catalogDuckLake
627+
result.LogicalCatalog = requestedCatalog
628+
result.CatalogValid = true
629+
}
630+
600631
// Authenticate the user within the resolved org. Minted service
601632
// credentials (svc_-prefixed usernames) resolve against the grants
602633
// snapshot map ONLY — the service plane shares no storage with

controlplane/configstore/store_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,3 +830,98 @@ func TestWithSnapshotHoldsPublicationReadLock(t *testing.T) {
830830
t.Fatal("WithSnapshot did not invoke callback")
831831
}
832832
}
833+
834+
// TestResolvePostgresConnectionLogicalCatalog covers the logical catalog alias:
835+
// a session may name its org's Trino catalog (`org_<database_name>`) as the
836+
// startup `database` and get the SAME physical DuckLake catalog under that
837+
// name. The alias is validated AGAINST the org the managed hostname already
838+
// resolved — it is never a lookup key, so it cannot route anywhere (PR #651).
839+
func TestResolvePostgresConnectionLogicalCatalog(t *testing.T) {
840+
cs := &ConfigStore{
841+
snapshot: &Snapshot{
842+
Orgs: map[string]*OrgConfig{
843+
"acme": {Name: "acme", DatabaseName: "acme-analytics"},
844+
"billing": {Name: "billing", DatabaseName: "billing_db"},
845+
},
846+
DatabaseOrg: map[string]string{
847+
"acme-analytics": "acme",
848+
"billing_db": "billing",
849+
},
850+
OrgUserPassword: map[OrgUserKey]string{
851+
{OrgID: "acme", Username: "root"}: mustHash(t, "secret"),
852+
{OrgID: "billing", Username: "root"}: mustHash(t, "secret"),
853+
},
854+
},
855+
}
856+
857+
t.Run("org catalog name selects the physical catalog under the logical name", func(t *testing.T) {
858+
got := cs.ResolvePostgresConnection("org_acme_analytics", "acme-analytics", true, "root", "secret")
859+
if !got.CatalogValid {
860+
t.Fatalf("org catalog name must be selectable: %+v", got)
861+
}
862+
if got.EffectiveCatalog != "ducklake" {
863+
t.Fatalf("EffectiveCatalog = %q, want ducklake (execution stays physical): %+v", got.EffectiveCatalog, got)
864+
}
865+
if got.LogicalCatalog != "org_acme_analytics" {
866+
t.Fatalf("LogicalCatalog = %q, want org_acme_analytics: %+v", got.LogicalCatalog, got)
867+
}
868+
if !got.Valid || got.OrgID != "acme" {
869+
t.Fatalf("unexpected auth result: %+v", got)
870+
}
871+
})
872+
873+
t.Run("mixed case and surrounding space normalize to the canonical name", func(t *testing.T) {
874+
got := cs.ResolvePostgresConnection(" ORG_Acme_Analytics ", "acme-analytics", true, "root", "secret")
875+
if !got.CatalogValid || got.LogicalCatalog != "org_acme_analytics" {
876+
t.Fatalf("catalog = (valid=%v, logical=%q), want the canonical lowercase name: %+v",
877+
got.CatalogValid, got.LogicalCatalog, got)
878+
}
879+
})
880+
881+
t.Run("ducklake and empty carry no logical name", func(t *testing.T) {
882+
for _, db := range []string{"", "ducklake"} {
883+
got := cs.ResolvePostgresConnection(db, "acme-analytics", true, "root", "secret")
884+
if !got.CatalogValid || got.LogicalCatalog != "" {
885+
t.Fatalf("database %q: catalog = (valid=%v, logical=%q), want valid with no logical name: %+v",
886+
db, got.CatalogValid, got.LogicalCatalog, got)
887+
}
888+
}
889+
})
890+
891+
t.Run("an arbitrary name still fails closed", func(t *testing.T) {
892+
for _, db := range []string{"postgres", "org_", "org_nope", "acme-analytics", "acme"} {
893+
got := cs.ResolvePostgresConnection(db, "acme-analytics", true, "root", "secret")
894+
if got.CatalogValid || got.LogicalCatalog != "" {
895+
t.Fatalf("database %q must fail closed: %+v", db, got)
896+
}
897+
}
898+
})
899+
900+
t.Run("another org's catalog name is refused", func(t *testing.T) {
901+
// The security case: SNI authenticates acme, so billing's catalog name
902+
// must not be selectable — not even with acme's valid credentials.
903+
got := cs.ResolvePostgresConnection("org_billing_db", "acme-analytics", true, "root", "secret")
904+
if got.CatalogValid || got.LogicalCatalog != "" {
905+
t.Fatalf("a sibling org's catalog name must fail closed: %+v", got)
906+
}
907+
if got.OrgID != "acme" {
908+
t.Fatalf("OrgID = %q, want acme — identity still comes from SNI alone", got.OrgID)
909+
}
910+
})
911+
912+
t.Run("without managed SNI there is no org to validate against", func(t *testing.T) {
913+
// No SNI-resolved org means no catalog name to compare to, so the alias
914+
// cannot be accepted. The startup database must never resolve an org.
915+
got := cs.ResolvePostgresConnection("org_acme_analytics", "acme-analytics", false, "root", "secret")
916+
if got.CatalogValid || got.SNIResolved {
917+
t.Fatalf("logical alias must not be accepted without managed SNI: %+v", got)
918+
}
919+
})
920+
921+
t.Run("unknown managed hostname refuses the alias", func(t *testing.T) {
922+
got := cs.ResolvePostgresConnection("org_acme_analytics", "ghostorg", true, "root", "secret")
923+
if got.CatalogValid || got.SNIResolved || got.OrgID != "" {
924+
t.Fatalf("unknown SNI must not admit a logical alias: %+v", got)
925+
}
926+
})
927+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package configstore
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
)
7+
8+
// trinoCatalogIdentifier is the Trino catalog identifier grammar
9+
// ([a-z0-9_]+). Anything outside this set in the principal is replaced with
10+
// `_` before forming the catalog name.
11+
var trinoCatalogIdentifier = regexp.MustCompile(`[^a-z0-9_]`)
12+
13+
// TrinoSanitize lowercases and replaces non-[a-z0-9_] runs with `_`.
14+
// Pure function so callers can recover the sanitized name without holding a
15+
// provisioner.
16+
func TrinoSanitize(principal string) string {
17+
return trinoCatalogIdentifier.ReplaceAllString(strings.ToLower(principal), "_")
18+
}
19+
20+
// TrinoCatalogName returns the catalog identifier for an org.
21+
// Format: org_<sanitized>. The sanitization maps the org's TrinoPrincipal
22+
// (its database_name) to Trino identifier rules ([a-z0-9_]); any other
23+
// characters collapse to underscores.
24+
//
25+
// For principals that satisfy ValidateDatabaseName the mapping is injective
26+
// — that grammar allows only lowercase alphanumerics and hyphens, so the
27+
// hyphen is the only character rewritten and no valid principal contains the
28+
// underscore it becomes — which, with database_name's global unique index,
29+
// makes distinct orgs' catalog names distinct by construction. Grandfathered
30+
// rows predate the validation and can still converge; the Trino provisioner's
31+
// rejectPrincipalCollisions holds those orgs back rather than letting one read
32+
// the other's catalog.
33+
//
34+
// The name carried an `_iceberg` suffix while the backing table format was
35+
// Iceberg behind Lakekeeper. Warehouses are DuckLake now (migration 000014
36+
// dropped every iceberg_* column), so the suffix went with it. The shape is
37+
// pinned from three sides — this function, opa.ManagedCatalogPattern, and
38+
// the regex literal inside policy.rego — and the pair of tests named in
39+
// ManagedCatalogPattern's doc comment fails if any one of them moves alone.
40+
//
41+
// It lives here, not in the (kubernetes-tagged) Trino provisioner, because
42+
// ResolvePostgresConnection needs it in every build: the same name is the
43+
// logical catalog alias a pgwire session may connect with, so SQLMesh and
44+
// friends see ONE catalog name across the Duckgres and Trino engines.
45+
// provisioner.TrinoCatalogName delegates here.
46+
func TrinoCatalogName(principal string) string {
47+
return "org_" + TrinoSanitize(principal)
48+
}

0 commit comments

Comments
 (0)