From bfc835c7a9a42315547cba774f64fd393ec63c84 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Sat, 8 Aug 2026 01:00:16 +0000 Subject: [PATCH] sync: port default-bucket resolution and the parent-key node descent from constructive-db MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit constructive-db's vendored copy moved ahead again in two packages. Ports the newer side upstream, unchanged, and regenerates both bundles. function-resolution: bucket_matches, default_bucket_tag and resolve_default_bucket (new triples), a resolve_bucket rewritten on top of bucket_matches, and the errors dependency the new code raises through. object-store: insert_nodes_at_paths' dirty-directory descent now joins a child to its parent on the parent's key instead of recursing on depth with a path-prefix filter — a real equijoin the planner hashes once per level, where the filter compared every dirty directory against every row of the level above (300s of a 307s pass on a 22k-directory batch, against 0.4s). Versions stay at 0.39.0 in both trees: the registry tops out at 0.36.0, so 0.39.0 is unpublished and the two trees agreeing on content under it is the point. Everything else that still differs between the trees is tree-specific — the pgsql-test/constructive-test harness import and the pgpm devDependency pin. --- .../__tests__/default-bucket.test.ts | 191 +++++++++++++++++ .../procedures/bucket_matches.sql | 94 +++++++++ .../procedures/default_bucket_tag.sql | 26 +++ .../procedures/resolve_bucket.sql | 73 ++----- .../procedures/resolve_default_bucket.sql | 129 ++++++++++++ packages/function-resolution/package.json | 1 + .../pgpm-function-resolution.control | 2 +- packages/function-resolution/pgpm.plan | 5 +- .../procedures/bucket_matches.sql | 7 + .../procedures/default_bucket_tag.sql | 7 + .../procedures/resolve_default_bucket.sql | 7 + ...-function-resolution--0.39.0.bundle.tar.gz | Bin 25301 -> 28230 bytes .../sql/pgpm-function-resolution--0.39.0.sql | 198 +++++++++++++++--- .../procedures/bucket_matches.sql | 7 + .../procedures/default_bucket_tag.sql | 7 + .../procedures/resolve_default_bucket.sql | 7 + .../procedures/insert_nodes_at_paths.sql | 33 +-- .../sql/object-store--0.39.0.bundle.tar.gz | Bin 18170 -> 18467 bytes .../object-store/sql/object-store--0.39.0.sql | 33 +-- pnpm-lock.yaml | 3 + 20 files changed, 718 insertions(+), 112 deletions(-) create mode 100644 packages/function-resolution/__tests__/default-bucket.test.ts create mode 100644 packages/function-resolution/deploy/schemas/function_resolution/procedures/bucket_matches.sql create mode 100644 packages/function-resolution/deploy/schemas/function_resolution/procedures/default_bucket_tag.sql create mode 100644 packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_default_bucket.sql create mode 100644 packages/function-resolution/revert/schemas/function_resolution/procedures/bucket_matches.sql create mode 100644 packages/function-resolution/revert/schemas/function_resolution/procedures/default_bucket_tag.sql create mode 100644 packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_default_bucket.sql create mode 100644 packages/function-resolution/verify/schemas/function_resolution/procedures/bucket_matches.sql create mode 100644 packages/function-resolution/verify/schemas/function_resolution/procedures/default_bucket_tag.sql create mode 100644 packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_default_bucket.sql diff --git a/packages/function-resolution/__tests__/default-bucket.test.ts b/packages/function-resolution/__tests__/default-bucket.test.ts new file mode 100644 index 00000000..60977c4f --- /dev/null +++ b/packages/function-resolution/__tests__/default-bucket.test.ts @@ -0,0 +1,191 @@ +import { getConnections, PgTestClient } from 'pgsql-test'; + +let pg: PgTestClient; +let teardown: () => Promise; + +// Deterministic fixture ids. +const PLATFORM_DB = '11111111-1111-1111-1111-111111111111'; +const TENANT_DB = '22222222-2222-2222-2222-222222222222'; +// A tenant that never labelled a default bucket. +const BARE_DB = '33333333-3333-3333-3333-333333333333'; +// A tenant that labelled two, which is a tagging mistake and not a coin flip. +const AMBIGUOUS_DB = '44444444-4444-4444-4444-444444444444'; +// A tenant whose own default must never answer somebody else's probe. +const OTHER_DB = '55555555-5555-5555-5555-555555555555'; + +const ids: Record = {}; + +// The database's default bucket: the answer to "store this file" when no client +// named a bucket. It is a labelled bucket resolved by the same exactly-one rule +// capabilities use — never a literal, never an environment setting, never a +// fallback to another tenant's storage. +describe('default bucket resolution', () => { + beforeAll(async () => { + ({ pg, teardown } = await getConnections()); + + await pg.query( + `INSERT INTO metaschema_public.database (id, name, platform) + VALUES ($1, 'platform_db', true), ($2, 'tenant_db', false), + ($3, 'bare_db', false), ($4, 'ambiguous_db', false), + ($5, 'other_db', false)`, + [PLATFORM_DB, TENANT_DB, BARE_DB, AMBIGUOUS_DB, OTHER_DB] + ); + + // The published bucket plane, as the catalog-sync triggers maintain it. + await pg.query(`CREATE SCHEMA catalog_private`); + await pg.query( + `CREATE TABLE catalog_private.buckets ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + owner_scope text NOT NULL, + owner_key uuid, + is_visible boolean NOT NULL DEFAULT false, + database_id uuid NOT NULL, + key text NOT NULL, + type text NOT NULL, + physical_name text, + tags text[] + )` + ); + + const bucket = async ( + database: string, + key: string, + type: string, + tags: string[] + ) => { + const row = await pg.one( + `INSERT INTO catalog_private.buckets + (owner_scope, owner_key, is_visible, database_id, key, type, physical_name, tags) + VALUES ('database', $1, false, $1, $2, $3, $4, $5) RETURNING id`, + [database, key, type, `phys-${key}`, tags] + ); + return row.id; + }; + + ids.default = await bucket(TENANT_DB, 'default', 'private', ['default']); + ids.defaultPublic = await bucket(TENANT_DB, 'default-public', 'public', [ + 'default-public', + ]); + ids.avatars = await bucket(TENANT_DB, 'avatars', 'public', ['avatars']); + + ids.otherDefault = await bucket(OTHER_DB, 'default', 'private', ['default']); + + ids.ambiguousOne = await bucket(AMBIGUOUS_DB, 'documents', 'private', [ + 'default', + ]); + ids.ambiguousTwo = await bucket(AMBIGUOUS_DB, 'uploads', 'private', [ + 'default', + ]); + }); + + afterAll(async () => { + await teardown(); + }); + + it('the reserved tag vocabulary is one fact, not a literal per call site', async () => { + const [tags] = await pg.any( + `SELECT function_resolution.default_bucket_tag(false) AS private_tag, + function_resolution.default_bucket_tag(true) AS public_tag` + ); + expect(tags).toEqual({ private_tag: 'default', public_tag: 'default-public' }); + }); + + it('resolves the database default without anyone naming a bucket', async () => { + const [row] = await pg.any( + `SELECT bucket_id, resolved_key, bucket_type, physical_name, owner_database_id + FROM function_resolution.resolve_default_bucket($1, 'database', $1, false)`, + [TENANT_DB] + ); + expect(row).toEqual({ + bucket_id: ids.default, + resolved_key: 'default', + bucket_type: 'private', + physical_name: 'phys-default', + owner_database_id: TENANT_DB, + }); + }); + + it('public access resolves the CDN-served default, not the private one', async () => { + const [row] = await pg.any( + `SELECT bucket_id, bucket_type + FROM function_resolution.resolve_default_bucket($1, 'database', $1, true)`, + [TENANT_DB] + ); + expect(row).toEqual({ bucket_id: ids.defaultPublic, bucket_type: 'public' }); + }); + + it('an explicit logical key overrides the default and resolves by the same rule', async () => { + const [row] = await pg.any( + `SELECT bucket_id, resolved_key + FROM function_resolution.resolve_default_bucket($1, 'database', $1, false, 'avatars')`, + [TENANT_DB] + ); + expect(row).toEqual({ bucket_id: ids.avatars, resolved_key: 'avatars' }); + }); + + it('an explicit key nothing carries raises rather than falling back to the default', async () => { + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve_default_bucket($1, 'database', $1, false, 'nope')`, + [TENANT_DB] + ) + ).rejects.toThrow(/STORAGE_DEFAULT_BUCKET_NOT_FOUND/); + }); + + it('a blank key is a caller bug, not a request for the default', async () => { + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve_default_bucket($1, 'database', $1, false, ' ')`, + [TENANT_DB] + ) + ).rejects.toThrow(/STORAGE_BUCKET_KEY_BLANK/); + }); + + it('a database with no default bucket raises', async () => { + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve_default_bucket($1, 'database', $1, false)`, + [BARE_DB] + ) + ).rejects.toThrow(/STORAGE_DEFAULT_BUCKET_NOT_FOUND/); + }); + + it('another tenant default is never borrowed', async () => { + // OTHER_DB has a bucket tagged 'default'; BARE_DB still has none. + const [foreign] = await pg.any( + `SELECT bucket_id FROM function_resolution.resolve_default_bucket($1, 'database', $1, false)`, + [OTHER_DB] + ); + expect(foreign.bucket_id).toBe(ids.otherDefault); + + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve_default_bucket($1, 'database', $1, false)`, + [BARE_DB] + ) + ).rejects.toThrow(/STORAGE_DEFAULT_BUCKET_NOT_FOUND/); + }); + + it('two default buckets raise, naming the candidates', async () => { + let failure: (Error & { detail?: string }) | null = null; + try { + await pg.any( + `SELECT * FROM function_resolution.resolve_default_bucket($1, 'database', $1, false)`, + [AMBIGUOUS_DB] + ); + } catch (error) { + failure = error as Error & { detail?: string }; + } + + expect(failure).not.toBeNull(); + expect(failure!.message).toMatch(/STORAGE_DEFAULT_BUCKET_AMBIGUOUS/); + + const detail = JSON.parse(failure!.detail!); + expect(detail.code).toBe('STORAGE_DEFAULT_BUCKET_AMBIGUOUS'); + expect(detail.context.tag).toBe('default'); + expect(detail.context.candidates).toEqual([ + { bucket_id: ids.ambiguousOne, key: 'documents', type: 'private' }, + { bucket_id: ids.ambiguousTwo, key: 'uploads', type: 'private' }, + ]); + }); +}); diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/bucket_matches.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/bucket_matches.sql new file mode 100644 index 00000000..2d3dca9f --- /dev/null +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/bucket_matches.sql @@ -0,0 +1,94 @@ +-- Deploy schemas/function_resolution/procedures/bucket_matches to pg + +-- requires: schemas/function_resolution/schema +-- requires: schemas/function_resolution/procedures/frame_candidates + +BEGIN; + +-- bucket_matches: every bucket the nearest answering frame offers for a +-- {tags, type} selector — the lookup half of bucket resolution, with no opinion +-- about how many rows are acceptable. +-- +-- Split out so that "which buckets match" has exactly one implementation while +-- each caller states its own arity rule: resolve_bucket demands one for a +-- capability declaration, resolve_default_bucket demands one for a database's +-- default, and both raise on zero or several. A second copy of this query would +-- be a second answer to "which bucket is this". +-- +-- One static set-based query answers every frame at once. The shared plane holds +-- every logical database's rows, so each candidate carries the row's expected +-- database_id — the candidate's own key at database scope, the frame's lookup +-- database otherwise. Without it one tenant's probe could be answered by another +-- tenant's row. +-- +-- Candidates are probed most-specific frame first and only the nearest frame +-- that answered contributes rows: a nearer frame outranks an outer one, and ties +-- within that frame are the ambiguity a caller raises on. +-- +-- Cross-scope reach follows the catalog's own visibility rule: a bucket owned by +-- another database matches only when is_visible (propagated from the source +-- row's is_public), so an outer frame's private bucket is unreachable. +-- +-- A frame database without a buckets catalog simply contributes no candidates: +-- storage is an optional module, so its absence is not a provisioning error the +-- way a missing functions catalog is. +-- +-- plpgsql, not sql: this module is portable and deploys into databases that host +-- no catalog module, so catalog_private must be resolved on first call rather +-- than at CREATE FUNCTION time. +CREATE FUNCTION function_resolution.bucket_matches( + database_id uuid, + scope text, + entity_id uuid, + tags text[], + type_filter text DEFAULT NULL +) RETURNS TABLE ( + bucket_id uuid, + bucket_key text, + bucket_type text, + physical_name text, + owner_database_id uuid, + owner_scope text, + owner_key uuid +) AS $$ +BEGIN + RETURN QUERY + WITH hits AS ( + SELECT b.id, + b.key, + b.type, + b.physical_name, + b.database_id, + b.owner_scope, + b.owner_key, + cand.ord + FROM function_resolution.frame_candidates( + bucket_matches.database_id, + bucket_matches.scope, + bucket_matches.entity_id + ) cand + JOIN catalog_private.buckets b + ON b.owner_scope = cand.owner_scope + AND b.owner_key IS NOT DISTINCT FROM cand.owner_key + AND b.database_id = CASE + WHEN cand.owner_scope = 'database' THEN cand.owner_key + ELSE cand.lookup_database_id + END + WHERE b.tags @> bucket_matches.tags + AND (bucket_matches.type_filter IS NULL OR b.type = bucket_matches.type_filter) + AND (b.database_id = bucket_matches.database_id OR b.is_visible) + ) + SELECT h.id, + h.key, + h.type, + h.physical_name, + h.database_id, + h.owner_scope, + h.owner_key + FROM hits h + WHERE h.ord = (SELECT min(hh.ord) FROM hits hh) + ORDER BY h.id; +END; +$$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +COMMIT; diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/default_bucket_tag.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/default_bucket_tag.sql new file mode 100644 index 00000000..fc65fe6c --- /dev/null +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/default_bucket_tag.sql @@ -0,0 +1,26 @@ +-- Deploy schemas/function_resolution/procedures/default_bucket_tag to pg + +-- requires: schemas/function_resolution/schema + +BEGIN; + +-- default_bucket_tag: the reserved tag vocabulary for "the bucket this database +-- uses when nobody named one", in one place. +-- +-- A database's default bucket is not a column, a setting or a flag: it is a +-- bucket a tenant labelled, resolved by the same tag rule capabilities use. +-- Two reserved labels, because "the default" is two questions: +-- default -- the private default (presigned GET) +-- default-public -- the CDN-served default (public reads) +-- +-- Reserved only by convention plus provisioning: storage bootstrap applies them, +-- and resolution reads them. A tenant that retags its own buckets changes which +-- bucket is default, and a tenant that applies a label twice gets a loud +-- ambiguity rather than a silent winner. +CREATE FUNCTION function_resolution.default_bucket_tag( + public_access boolean +) RETURNS text AS $$ + SELECT CASE WHEN default_bucket_tag.public_access THEN 'default-public' ELSE 'default' END; +$$ LANGUAGE sql IMMUTABLE; + +COMMIT; diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_bucket.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_bucket.sql index 6a8afa17..ae0f58b8 100644 --- a/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_bucket.sql +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_bucket.sql @@ -2,6 +2,7 @@ -- requires: schemas/function_resolution/schema -- requires: schemas/function_resolution/procedures/frame_candidates +-- requires: schemas/function_resolution/procedures/bucket_matches BEGIN; @@ -16,25 +17,15 @@ BEGIN; -- uploads / variants / exports are documentation, not DDL: nothing in the -- schema constrains a tenant to one bucket per tag. -- --- Determinism is enforced here rather than by a unique index: candidates are --- probed most-specific frame first, and the winning frame must answer with --- exactly one bucket. Zero matches and several matches both raise, and the +-- Determinism is enforced here rather than by a unique index: bucket_matches +-- probes candidates most-specific frame first, and the winning frame must answer +-- with exactly one bucket. Zero matches and several matches both raise, and the -- ambiguous error names the candidates so the fix (retag, narrow by type, or -- write an explicit capability binding) is obvious. -- --- Cross-scope reach follows the catalog's own visibility rule: a bucket owned --- by another database resolves only when is_visible (propagated from the --- source row's is_public), so an outer frame's private bucket is unreachable. --- --- One static set-based query answers every frame at once. The shared plane holds --- every logical database's rows, so each candidate carries the row's expected --- database_id — the candidate's own key at database scope, the frame's lookup --- database otherwise — exactly as resolve() does for functions. Without it one --- tenant's probe could be answered by another tenant's row. --- --- A frame database without a buckets catalog simply contributes no candidates: --- storage is an optional module, so its absence is not a provisioning error the --- way a missing functions catalog is. +-- The match itself — frames, same-tenant proof, cross-scope visibility, and the +-- absent-catalog case — lives in bucket_matches, so a capability declaration and +-- a database's default bucket are answered by one query under one arity rule. CREATE FUNCTION function_resolution.resolve_bucket( database_id uuid, scope text, @@ -63,39 +54,15 @@ BEGIN -- Every frame in one indexed read, keeping only the matches of the most -- specific frame that answered: a nearer frame outranks an outer one, and -- ties within that frame are the ambiguity raised below. - WITH hits AS ( - SELECT b.id, - b.key, - b.type, - b.physical_name, - b.database_id, - b.owner_scope, - b.owner_key, - cand.ord - FROM function_resolution.frame_candidates( - resolve_bucket.database_id, - resolve_bucket.scope, - resolve_bucket.entity_id - ) cand - JOIN catalog_private.buckets b - ON b.owner_scope = cand.owner_scope - AND b.owner_key IS NOT DISTINCT FROM cand.owner_key - AND b.database_id = CASE - WHEN cand.owner_scope = 'database' THEN cand.owner_key - ELSE cand.lookup_database_id - END - WHERE b.tags @> resolve_bucket.tags - AND (resolve_bucket.type_filter IS NULL OR b.type = resolve_bucket.type_filter) - AND (b.database_id = resolve_bucket.database_id OR b.is_visible) - ), - nearest AS ( - SELECT h.* - FROM hits h - WHERE h.ord = (SELECT min(hh.ord) FROM hits hh) - ) - SELECT COALESCE(jsonb_agg(to_jsonb(n) ORDER BY n.id), '[]'::jsonb) + SELECT COALESCE(jsonb_agg(to_jsonb(m) ORDER BY m.bucket_id), '[]'::jsonb) INTO v_matches - FROM nearest n; + FROM function_resolution.bucket_matches( + resolve_bucket.database_id, + resolve_bucket.scope, + resolve_bucket.entity_id, + resolve_bucket.tags, + resolve_bucket.type_filter + ) m; IF jsonb_array_length(v_matches) = 0 THEN RAISE EXCEPTION 'CAPABILITY_BUCKET_NOT_FOUND: no bucket tagged % % resolves in the scope chain starting at scope "%" (database_id=%)', @@ -111,18 +78,18 @@ BEGIN jsonb_array_length(v_matches), resolve_bucket.tags, COALESCE('of type ' || resolve_bucket.type_filter, '(any type)'), - (SELECT string_agg(format('%s (%s)', m->>'key', m->>'id'), ', ' ORDER BY m->>'key') + (SELECT string_agg(format('%s (%s)', m->>'bucket_key', m->>'bucket_id'), ', ' ORDER BY m->>'bucket_key') FROM jsonb_array_elements(v_matches) m) USING ERRCODE = 'FR012'; END IF; v_match := v_matches->0; - resolve_bucket.bucket_id := (v_match->>'id')::uuid; - resolve_bucket.bucket_key := v_match->>'key'; - resolve_bucket.bucket_type := v_match->>'type'; + resolve_bucket.bucket_id := (v_match->>'bucket_id')::uuid; + resolve_bucket.bucket_key := v_match->>'bucket_key'; + resolve_bucket.bucket_type := v_match->>'bucket_type'; resolve_bucket.physical_name := v_match->>'physical_name'; - resolve_bucket.owner_database_id := (v_match->>'database_id')::uuid; + resolve_bucket.owner_database_id := (v_match->>'owner_database_id')::uuid; resolve_bucket.owner_scope := v_match->>'owner_scope'; resolve_bucket.owner_key := (v_match->>'owner_key')::uuid; diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_default_bucket.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_default_bucket.sql new file mode 100644 index 00000000..e911199a --- /dev/null +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_default_bucket.sql @@ -0,0 +1,129 @@ +-- Deploy schemas/function_resolution/procedures/resolve_default_bucket to pg + +-- requires: schemas/function_resolution/schema +-- requires: schemas/function_resolution/procedures/bucket_matches +-- requires: schemas/function_resolution/procedures/default_bucket_tag + +BEGIN; + +-- resolve_default_bucket: answer "which bucket does this database store into +-- when the caller did not name one", server-side. +-- +-- Bucket choice belongs to the database, never to a client: a client-chosen +-- string means a different bucket per tenant, and an environment-level bucket +-- means storage that does not belong to any tenant at all. So there is no +-- global fallback here and no default that resolves outside the execution's own +-- frame chain — a database either labelled a default bucket or it has none, and +-- the second case raises. +-- +-- Two routes, one rule: +-- * bucket_key given -- the per-field override. A logical key is a label, the +-- same vocabulary a function's required_buckets declares, so the override +-- resolves through the identical match: nothing here is key-specific. +-- * bucket_key omitted -- the reserved default tag for the requested access +-- (see default_bucket_tag): 'default' or 'default-public'. +-- +-- Either way the match must be exactly one bucket. Zero raises, several raise +-- naming the candidates, and nothing is guessed in between: an ambiguous default +-- is a tenant's tagging mistake, and picking one would silently write a tenant's +-- files into whichever bucket sorted first. +CREATE FUNCTION function_resolution.resolve_default_bucket( + database_id uuid, + scope text, + entity_id uuid, + public_access boolean, + bucket_key text DEFAULT NULL +) RETURNS TABLE ( + bucket_id uuid, + resolved_key text, + bucket_type text, + physical_name text, + owner_database_id uuid, + owner_scope text, + owner_key uuid +) AS $$ +DECLARE + v_tag text; + v_matches jsonb; + v_match jsonb; +BEGIN + -- A blank override is a caller bug, not a request for the default: falling + -- through to tag resolution would turn a broken field binding into a + -- silently different bucket. + IF resolve_default_bucket.bucket_key IS NOT NULL + AND btrim(resolve_default_bucket.bucket_key) = '' THEN + PERFORM errors.raise_error( + 'STORAGE_BUCKET_KEY_BLANK', + jsonb_build_object( + 'database_id', resolve_default_bucket.database_id, + 'scope', resolve_default_bucket.scope + ), + 'internal' + ); + END IF; + + v_tag := COALESCE( + resolve_default_bucket.bucket_key, + function_resolution.default_bucket_tag(resolve_default_bucket.public_access) + ); + + SELECT COALESCE(jsonb_agg(to_jsonb(m) ORDER BY m.bucket_id), '[]'::jsonb) + INTO v_matches + FROM function_resolution.bucket_matches( + resolve_default_bucket.database_id, + resolve_default_bucket.scope, + resolve_default_bucket.entity_id, + ARRAY[v_tag] + ) m; + + IF jsonb_array_length(v_matches) = 0 THEN + PERFORM errors.raise_error( + 'STORAGE_DEFAULT_BUCKET_NOT_FOUND', + jsonb_build_object( + 'database_id', resolve_default_bucket.database_id, + 'scope', resolve_default_bucket.scope, + 'entity_id', resolve_default_bucket.entity_id, + 'tag', v_tag, + 'explicit_key', resolve_default_bucket.bucket_key IS NOT NULL + ), + 'internal' + ); + END IF; + + IF jsonb_array_length(v_matches) > 1 THEN + PERFORM errors.raise_error( + 'STORAGE_DEFAULT_BUCKET_AMBIGUOUS', + jsonb_build_object( + 'database_id', resolve_default_bucket.database_id, + 'scope', resolve_default_bucket.scope, + 'entity_id', resolve_default_bucket.entity_id, + 'tag', v_tag, + 'explicit_key', resolve_default_bucket.bucket_key IS NOT NULL, + 'candidates', ( + SELECT jsonb_agg(jsonb_build_object( + 'bucket_id', c->>'bucket_id', + 'key', c->>'bucket_key', + 'type', c->>'bucket_type' + ) ORDER BY c->>'bucket_key') + FROM jsonb_array_elements(v_matches) c + ) + ), + 'internal' + ); + END IF; + + v_match := v_matches->0; + + resolve_default_bucket.bucket_id := (v_match->>'bucket_id')::uuid; + resolve_default_bucket.resolved_key := v_match->>'bucket_key'; + resolve_default_bucket.bucket_type := v_match->>'bucket_type'; + resolve_default_bucket.physical_name := v_match->>'physical_name'; + resolve_default_bucket.owner_database_id := (v_match->>'owner_database_id')::uuid; + resolve_default_bucket.owner_scope := v_match->>'owner_scope'; + resolve_default_bucket.owner_key := (v_match->>'owner_key')::uuid; + + RETURN NEXT; +END; +$$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +COMMIT; diff --git a/packages/function-resolution/package.json b/packages/function-resolution/package.json index fd23019b..1661733f 100644 --- a/packages/function-resolution/package.json +++ b/packages/function-resolution/package.json @@ -26,6 +26,7 @@ "dependencies": { "@pgpm/app-scope": "workspace:*", "@pgpm/database-jobs": "workspace:*", + "@pgpm/errors": "workspace:*", "@pgpm/jwt-claims": "workspace:*", "@pgpm/metaschema-modules": "workspace:*", "@pgpm/metaschema-schema": "workspace:*", diff --git a/packages/function-resolution/pgpm-function-resolution.control b/packages/function-resolution/pgpm-function-resolution.control index 78023aba..18a4722b 100644 --- a/packages/function-resolution/pgpm-function-resolution.control +++ b/packages/function-resolution/pgpm-function-resolution.control @@ -2,6 +2,6 @@ comment = 'pgpm-function-resolution extension' default_version = '0.39.0' module_pathname = '$libdir/pgpm-function-resolution' -requires = 'plpgsql,pgpm-verify,metaschema-schema,metaschema-modules,pgpm-app-scope,pgpm-database-jobs,pgpm-jwt-claims' +requires = 'plpgsql,errors,pgpm-verify,metaschema-schema,metaschema-modules,pgpm-app-scope,pgpm-database-jobs,pgpm-jwt-claims' relocatable = false superuser = false diff --git a/packages/function-resolution/pgpm.plan b/packages/function-resolution/pgpm.plan index b98f1fd8..3c9a1435 100644 --- a/packages/function-resolution/pgpm.plan +++ b/packages/function-resolution/pgpm.plan @@ -9,7 +9,10 @@ schemas/function_resolution/procedures/resolve [schemas/function_resolution/sche schemas/function_resolution/procedures/resolve_invocation [schemas/function_resolution/schema schemas/function_resolution/procedures/resolve] 2017-08-11T08:11:51Z constructive # invocation-lane resolver schemas/function_resolution/procedures/enqueue [schemas/function_resolution/schema schemas/function_resolution/procedures/routing schemas/function_resolution/procedures/resolve] 2017-08-11T08:11:51Z constructive # resolver-aware enqueue entry point schemas/function_resolution/procedures/frame_candidates [schemas/function_resolution/schema] 2017-08-11T08:11:51Z constructive # frames expanded into catalog probe candidates -schemas/function_resolution/procedures/resolve_bucket [schemas/function_resolution/schema schemas/function_resolution/procedures/frame_candidates] 2017-08-11T08:11:51Z constructive # bucket selector {tags,type} resolution +schemas/function_resolution/procedures/bucket_matches [schemas/function_resolution/schema schemas/function_resolution/procedures/frame_candidates] 2017-08-11T08:11:51Z constructive # buckets a selector matches in the nearest frame +schemas/function_resolution/procedures/resolve_bucket [schemas/function_resolution/schema schemas/function_resolution/procedures/frame_candidates schemas/function_resolution/procedures/bucket_matches] 2017-08-11T08:11:51Z constructive # bucket selector {tags,type} resolution +schemas/function_resolution/procedures/default_bucket_tag [schemas/function_resolution/schema] 2017-08-11T08:11:51Z constructive # reserved default-bucket tag vocabulary +schemas/function_resolution/procedures/resolve_default_bucket [schemas/function_resolution/schema schemas/function_resolution/procedures/bucket_matches schemas/function_resolution/procedures/default_bucket_tag] 2017-08-11T08:11:51Z constructive # the database's default bucket, or an explicit key schemas/function_resolution/procedures/bucket_catalog_row [schemas/function_resolution/schema schemas/function_resolution/procedures/frame_candidates] 2017-08-11T08:11:51Z constructive # reachable bucket by id (same-tenant proof) schemas/function_resolution/procedures/api_catalog_row [schemas/function_resolution/schema schemas/function_resolution/procedures/frame_candidates] 2017-08-11T08:11:51Z constructive # reachable api by id schemas/function_resolution/procedures/resolve_api [schemas/function_resolution/schema schemas/function_resolution/procedures/frame_candidates schemas/function_resolution/procedures/api_catalog_row] 2017-08-11T08:11:51Z constructive # api selector module:/name: resolution diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/bucket_matches.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/bucket_matches.sql new file mode 100644 index 00000000..83052d28 --- /dev/null +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/bucket_matches.sql @@ -0,0 +1,7 @@ +-- Revert schemas/function_resolution/procedures/bucket_matches from pg + +BEGIN; + +DROP FUNCTION function_resolution.bucket_matches(uuid, text, uuid, text[], text); + +COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/default_bucket_tag.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/default_bucket_tag.sql new file mode 100644 index 00000000..2f943a65 --- /dev/null +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/default_bucket_tag.sql @@ -0,0 +1,7 @@ +-- Revert schemas/function_resolution/procedures/default_bucket_tag from pg + +BEGIN; + +DROP FUNCTION function_resolution.default_bucket_tag(boolean); + +COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_default_bucket.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_default_bucket.sql new file mode 100644 index 00000000..088579fd --- /dev/null +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_default_bucket.sql @@ -0,0 +1,7 @@ +-- Revert schemas/function_resolution/procedures/resolve_default_bucket from pg + +BEGIN; + +DROP FUNCTION function_resolution.resolve_default_bucket(uuid, text, uuid, boolean, text); + +COMMIT; diff --git a/packages/function-resolution/sql/pgpm-function-resolution--0.39.0.bundle.tar.gz b/packages/function-resolution/sql/pgpm-function-resolution--0.39.0.bundle.tar.gz index 150d8b98e3824d74d3598e2c4c94c6da607790b9..6d42e8a53ae217bbf030860d1d372d723b77d87b 100644 GIT binary patch literal 28230 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℘9)&%YC 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 literal 25301 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 IY$3q@2g-k_k^lez diff --git a/packages/function-resolution/sql/pgpm-function-resolution--0.39.0.sql b/packages/function-resolution/sql/pgpm-function-resolution--0.39.0.sql index b11f8e16..8a74261a 100644 --- a/packages/function-resolution/sql/pgpm-function-resolution--0.39.0.sql +++ b/packages/function-resolution/sql/pgpm-function-resolution--0.39.0.sql @@ -427,7 +427,7 @@ BEGIN END; $EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; -CREATE FUNCTION function_resolution.resolve_bucket( +CREATE FUNCTION function_resolution.bucket_matches( database_id uuid, scope text, entity_id uuid, @@ -442,19 +442,8 @@ CREATE FUNCTION function_resolution.resolve_bucket( owner_scope text, owner_key uuid ) AS $EOFCODE$ -DECLARE - v_matches jsonb; - v_match jsonb; BEGIN - IF resolve_bucket.tags IS NULL OR cardinality(resolve_bucket.tags) = 0 THEN - RAISE EXCEPTION 'CAPABILITY_BUCKET_SELECTOR_EMPTY: a bucket selector needs at least one tag (database_id=%, scope="%")', - resolve_bucket.database_id, resolve_bucket.scope - USING ERRCODE = 'FR010'; - END IF; - - -- Every frame in one indexed read, keeping only the matches of the most - -- specific frame that answered: a nearer frame outranks an outer one, and - -- ties within that frame are the ambiguity raised below. + RETURN QUERY WITH hits AS ( SELECT b.id, b.key, @@ -465,9 +454,9 @@ BEGIN b.owner_key, cand.ord FROM function_resolution.frame_candidates( - resolve_bucket.database_id, - resolve_bucket.scope, - resolve_bucket.entity_id + bucket_matches.database_id, + bucket_matches.scope, + bucket_matches.entity_id ) cand JOIN catalog_private.buckets b ON b.owner_scope = cand.owner_scope @@ -476,18 +465,60 @@ BEGIN WHEN cand.owner_scope = 'database' THEN cand.owner_key ELSE cand.lookup_database_id END - WHERE b.tags @> resolve_bucket.tags - AND (resolve_bucket.type_filter IS NULL OR b.type = resolve_bucket.type_filter) - AND (b.database_id = resolve_bucket.database_id OR b.is_visible) - ), - nearest AS ( - SELECT h.* - FROM hits h - WHERE h.ord = (SELECT min(hh.ord) FROM hits hh) + WHERE b.tags @> bucket_matches.tags + AND (bucket_matches.type_filter IS NULL OR b.type = bucket_matches.type_filter) + AND (b.database_id = bucket_matches.database_id OR b.is_visible) ) - SELECT COALESCE(jsonb_agg(to_jsonb(n) ORDER BY n.id), '[]'::jsonb) + SELECT h.id, + h.key, + h.type, + h.physical_name, + h.database_id, + h.owner_scope, + h.owner_key + FROM hits h + WHERE h.ord = (SELECT min(hh.ord) FROM hits hh) + ORDER BY h.id; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +CREATE FUNCTION function_resolution.resolve_bucket( + database_id uuid, + scope text, + entity_id uuid, + tags text[], + type_filter text DEFAULT NULL +) RETURNS TABLE ( + bucket_id uuid, + bucket_key text, + bucket_type text, + physical_name text, + owner_database_id uuid, + owner_scope text, + owner_key uuid +) AS $EOFCODE$ +DECLARE + v_matches jsonb; + v_match jsonb; +BEGIN + IF resolve_bucket.tags IS NULL OR cardinality(resolve_bucket.tags) = 0 THEN + RAISE EXCEPTION 'CAPABILITY_BUCKET_SELECTOR_EMPTY: a bucket selector needs at least one tag (database_id=%, scope="%")', + resolve_bucket.database_id, resolve_bucket.scope + USING ERRCODE = 'FR010'; + END IF; + + -- Every frame in one indexed read, keeping only the matches of the most + -- specific frame that answered: a nearer frame outranks an outer one, and + -- ties within that frame are the ambiguity raised below. + SELECT COALESCE(jsonb_agg(to_jsonb(m) ORDER BY m.bucket_id), '[]'::jsonb) INTO v_matches - FROM nearest n; + FROM function_resolution.bucket_matches( + resolve_bucket.database_id, + resolve_bucket.scope, + resolve_bucket.entity_id, + resolve_bucket.tags, + resolve_bucket.type_filter + ) m; IF jsonb_array_length(v_matches) = 0 THEN RAISE EXCEPTION 'CAPABILITY_BUCKET_NOT_FOUND: no bucket tagged % % resolves in the scope chain starting at scope "%" (database_id=%)', @@ -503,18 +534,18 @@ BEGIN jsonb_array_length(v_matches), resolve_bucket.tags, COALESCE('of type ' || resolve_bucket.type_filter, '(any type)'), - (SELECT string_agg(format('%s (%s)', m->>'key', m->>'id'), ', ' ORDER BY m->>'key') + (SELECT string_agg(format('%s (%s)', m->>'bucket_key', m->>'bucket_id'), ', ' ORDER BY m->>'bucket_key') FROM jsonb_array_elements(v_matches) m) USING ERRCODE = 'FR012'; END IF; v_match := v_matches->0; - resolve_bucket.bucket_id := (v_match->>'id')::uuid; - resolve_bucket.bucket_key := v_match->>'key'; - resolve_bucket.bucket_type := v_match->>'type'; + resolve_bucket.bucket_id := (v_match->>'bucket_id')::uuid; + resolve_bucket.bucket_key := v_match->>'bucket_key'; + resolve_bucket.bucket_type := v_match->>'bucket_type'; resolve_bucket.physical_name := v_match->>'physical_name'; - resolve_bucket.owner_database_id := (v_match->>'database_id')::uuid; + resolve_bucket.owner_database_id := (v_match->>'owner_database_id')::uuid; resolve_bucket.owner_scope := v_match->>'owner_scope'; resolve_bucket.owner_key := (v_match->>'owner_key')::uuid; @@ -522,6 +553,111 @@ BEGIN END; $EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; +CREATE FUNCTION function_resolution.default_bucket_tag( + public_access boolean +) RETURNS text AS $EOFCODE$ + SELECT CASE WHEN default_bucket_tag.public_access THEN 'default-public' ELSE 'default' END; +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION function_resolution.resolve_default_bucket( + database_id uuid, + scope text, + entity_id uuid, + public_access boolean, + bucket_key text DEFAULT NULL +) RETURNS TABLE ( + bucket_id uuid, + resolved_key text, + bucket_type text, + physical_name text, + owner_database_id uuid, + owner_scope text, + owner_key uuid +) AS $EOFCODE$ +DECLARE + v_tag text; + v_matches jsonb; + v_match jsonb; +BEGIN + -- A blank override is a caller bug, not a request for the default: falling + -- through to tag resolution would turn a broken field binding into a + -- silently different bucket. + IF resolve_default_bucket.bucket_key IS NOT NULL + AND btrim(resolve_default_bucket.bucket_key) = '' THEN + PERFORM errors.raise_error( + 'STORAGE_BUCKET_KEY_BLANK', + jsonb_build_object( + 'database_id', resolve_default_bucket.database_id, + 'scope', resolve_default_bucket.scope + ), + 'internal' + ); + END IF; + + v_tag := COALESCE( + resolve_default_bucket.bucket_key, + function_resolution.default_bucket_tag(resolve_default_bucket.public_access) + ); + + SELECT COALESCE(jsonb_agg(to_jsonb(m) ORDER BY m.bucket_id), '[]'::jsonb) + INTO v_matches + FROM function_resolution.bucket_matches( + resolve_default_bucket.database_id, + resolve_default_bucket.scope, + resolve_default_bucket.entity_id, + ARRAY[v_tag] + ) m; + + IF jsonb_array_length(v_matches) = 0 THEN + PERFORM errors.raise_error( + 'STORAGE_DEFAULT_BUCKET_NOT_FOUND', + jsonb_build_object( + 'database_id', resolve_default_bucket.database_id, + 'scope', resolve_default_bucket.scope, + 'entity_id', resolve_default_bucket.entity_id, + 'tag', v_tag, + 'explicit_key', resolve_default_bucket.bucket_key IS NOT NULL + ), + 'internal' + ); + END IF; + + IF jsonb_array_length(v_matches) > 1 THEN + PERFORM errors.raise_error( + 'STORAGE_DEFAULT_BUCKET_AMBIGUOUS', + jsonb_build_object( + 'database_id', resolve_default_bucket.database_id, + 'scope', resolve_default_bucket.scope, + 'entity_id', resolve_default_bucket.entity_id, + 'tag', v_tag, + 'explicit_key', resolve_default_bucket.bucket_key IS NOT NULL, + 'candidates', ( + SELECT jsonb_agg(jsonb_build_object( + 'bucket_id', c->>'bucket_id', + 'key', c->>'bucket_key', + 'type', c->>'bucket_type' + ) ORDER BY c->>'bucket_key') + FROM jsonb_array_elements(v_matches) c + ) + ), + 'internal' + ); + END IF; + + v_match := v_matches->0; + + resolve_default_bucket.bucket_id := (v_match->>'bucket_id')::uuid; + resolve_default_bucket.resolved_key := v_match->>'bucket_key'; + resolve_default_bucket.bucket_type := v_match->>'bucket_type'; + resolve_default_bucket.physical_name := v_match->>'physical_name'; + resolve_default_bucket.owner_database_id := (v_match->>'owner_database_id')::uuid; + resolve_default_bucket.owner_scope := v_match->>'owner_scope'; + resolve_default_bucket.owner_key := (v_match->>'owner_key')::uuid; + + RETURN NEXT; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + CREATE FUNCTION function_resolution.bucket_catalog_row( database_id uuid, scope text, diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/bucket_matches.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/bucket_matches.sql new file mode 100644 index 00000000..ab922637 --- /dev/null +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/bucket_matches.sql @@ -0,0 +1,7 @@ +-- Verify schemas/function_resolution/procedures/bucket_matches on pg + +BEGIN; + +SELECT assert_function('function_resolution.bucket_matches(uuid, text, uuid, text[], text)'::regprocedure); + +ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/default_bucket_tag.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/default_bucket_tag.sql new file mode 100644 index 00000000..ab4e8c08 --- /dev/null +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/default_bucket_tag.sql @@ -0,0 +1,7 @@ +-- Verify schemas/function_resolution/procedures/default_bucket_tag on pg + +BEGIN; + +SELECT assert_function('function_resolution.default_bucket_tag(boolean)'::regprocedure); + +ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_default_bucket.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_default_bucket.sql new file mode 100644 index 00000000..3ed76ac8 --- /dev/null +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_default_bucket.sql @@ -0,0 +1,7 @@ +-- Verify schemas/function_resolution/procedures/resolve_default_bucket on pg + +BEGIN; + +SELECT assert_function('function_resolution.resolve_default_bucket(uuid, text, uuid, boolean, text)'::regprocedure); + +ROLLBACK; diff --git a/packages/object-store/deploy/schemas/object_store_public/procedures/insert_nodes_at_paths.sql b/packages/object-store/deploy/schemas/object_store_public/procedures/insert_nodes_at_paths.sql index 2bca0171..6df26acc 100644 --- a/packages/object-store/deploy/schemas/object_store_public/procedures/insert_nodes_at_paths.sql +++ b/packages/object-store/deploy/schemas/object_store_public/procedures/insert_nodes_at_paths.sql @@ -187,31 +187,38 @@ BEGIN max_depth; -- 3. resolve each dirty directory against the pre-existing tree, walking down - -- from the current root so untouched siblings survive the rebuild + -- from the current root so untouched siblings survive the rebuild. + -- + -- The descent joins a directory to its parent on the parent's key, which + -- every directory already carries: an equijoin the planner hashes once per + -- level. Recursing on depth instead and matching with a path-prefix filter + -- (`child.path[1:r.depth] = r.path`) is the same walk but not a join + -- condition, so every dirty directory is compared against every row of the + -- level above — 9.6M filtered comparisons per level on a 22k-directory + -- batch, each re-deriving the path from the key, which is 300s of a 307s + -- pass against 0.4s for this one. + -- + -- The root is the descent's seed, not a child: it is its own parent under + -- `to_jsonb(path[1:0])`, so leaving it in the recursive side joins it to + -- itself forever. WITH RECURSIVE dirs AS ( SELECT dr.node_key, - dr.depth, - ARRAY ( - SELECT - jsonb_array_elements_text(dr.node_key::jsonb))::text[] AS path - FROM unnest(d_key, d_depth) AS dr (node_key, depth) + dr.name, + dr.parent + FROM unnest(d_key, d_name, d_parent) AS dr (node_key, name, parent) + WHERE dr.node_key <> root_key ), resolved AS ( SELECT root_key AS node_key, - 0 AS depth, - ARRAY[]::text[] AS path, insert_nodes_at_paths.root AS node_id UNION ALL SELECT child.node_key, - child.depth, - child.path, - parent_obj.kids[object_store_utils.array_index_of (parent_obj.ktree, child.path[child.depth])] + parent_obj.kids[object_store_utils.array_index_of (parent_obj.ktree, child.name)] FROM resolved AS r - JOIN dirs AS child ON child.depth = r.depth + 1 - AND child.path[1:r.depth] = r.path + JOIN dirs AS child ON child.parent = r.node_key LEFT JOIN object_store_public.object AS parent_obj ON parent_obj.id = r.node_id AND parent_obj.scope_id = insert_nodes_at_paths.s_id ) diff --git a/packages/object-store/sql/object-store--0.39.0.bundle.tar.gz b/packages/object-store/sql/object-store--0.39.0.bundle.tar.gz index c7402b60413de5d21d0902efd4117c0e9a52f32d..1734bec97a92aba866705e6ebaf899fc4e22e284 100644 GIT binary patch literal 18467 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 diff --git a/packages/object-store/sql/object-store--0.39.0.sql b/packages/object-store/sql/object-store--0.39.0.sql index 26dcf3ec..7dae5626 100644 --- a/packages/object-store/sql/object-store--0.39.0.sql +++ b/packages/object-store/sql/object-store--0.39.0.sql @@ -713,31 +713,38 @@ BEGIN max_depth; -- 3. resolve each dirty directory against the pre-existing tree, walking down - -- from the current root so untouched siblings survive the rebuild + -- from the current root so untouched siblings survive the rebuild. + -- + -- The descent joins a directory to its parent on the parent's key, which + -- every directory already carries: an equijoin the planner hashes once per + -- level. Recursing on depth instead and matching with a path-prefix filter + -- (`child.path[1:r.depth] = r.path`) is the same walk but not a join + -- condition, so every dirty directory is compared against every row of the + -- level above — 9.6M filtered comparisons per level on a 22k-directory + -- batch, each re-deriving the path from the key, which is 300s of a 307s + -- pass against 0.4s for this one. + -- + -- The root is the descent's seed, not a child: it is its own parent under + -- `to_jsonb(path[1:0])`, so leaving it in the recursive side joins it to + -- itself forever. WITH RECURSIVE dirs AS ( SELECT dr.node_key, - dr.depth, - ARRAY ( - SELECT - jsonb_array_elements_text(dr.node_key::jsonb))::text[] AS path - FROM unnest(d_key, d_depth) AS dr (node_key, depth) + dr.name, + dr.parent + FROM unnest(d_key, d_name, d_parent) AS dr (node_key, name, parent) + WHERE dr.node_key <> root_key ), resolved AS ( SELECT root_key AS node_key, - 0 AS depth, - ARRAY[]::text[] AS path, insert_nodes_at_paths.root AS node_id UNION ALL SELECT child.node_key, - child.depth, - child.path, - parent_obj.kids[object_store_utils.array_index_of (parent_obj.ktree, child.path[child.depth])] + parent_obj.kids[object_store_utils.array_index_of (parent_obj.ktree, child.name)] FROM resolved AS r - JOIN dirs AS child ON child.depth = r.depth + 1 - AND child.path[1:r.depth] = r.path + JOIN dirs AS child ON child.parent = r.node_key LEFT JOIN object_store_public.object AS parent_obj ON parent_obj.id = r.node_id AND parent_obj.scope_id = insert_nodes_at_paths.s_id ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8349ea3d..398c94be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -181,6 +181,9 @@ importers: '@pgpm/database-jobs': specifier: workspace:* version: link:../database-jobs + '@pgpm/errors': + specifier: workspace:* + version: link:../errors '@pgpm/jwt-claims': specifier: workspace:* version: link:../jwt-claims