Apply same composite-FK pattern as 001/002 for migration 003: - resources: add UNIQUE (id, space_id) as FK target. - resource_dependencies: denormalize space_id, composite FKs on both endpoints (enforces both ends of a dep live in the same space at the DB layer). - resource_credentials: denormalize space_id, composite FK to resources. - source_docs.resource_id: NOT NULL (tenancy anchor); resource_id was already absent from the update FIELDS list so docs cannot move resources. Repos derive space_id from the resource on addDependency/addCredential so callers can't fake cross-tenant assignment. 3 regression tests added.
76 lines
3.0 KiB
SQL
76 lines
3.0 KiB
SQL
CREATE TABLE resources (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
space_id uuid NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
|
|
slug text NOT NULL,
|
|
name text NOT NULL,
|
|
runtime_type text NOT NULL
|
|
CHECK (runtime_type IN ('lxc','vm','docker','bare-metal')),
|
|
host text,
|
|
url text,
|
|
version text,
|
|
status text NOT NULL DEFAULT 'unknown'
|
|
CHECK (status IN ('running','stopped','down','unknown')),
|
|
monitoring jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
last_check timestamptz,
|
|
maintenance_until timestamptz,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
UNIQUE (space_id, slug),
|
|
-- Composite key target for child tables enforcing same-space references
|
|
UNIQUE (id, space_id)
|
|
);
|
|
|
|
-- Both endpoints of a dependency must live in the same space.
|
|
CREATE TABLE resource_dependencies (
|
|
resource_id uuid NOT NULL,
|
|
depends_on uuid NOT NULL,
|
|
space_id uuid NOT NULL,
|
|
kind text,
|
|
PRIMARY KEY (resource_id, depends_on),
|
|
CHECK (resource_id <> depends_on),
|
|
FOREIGN KEY (resource_id, space_id) REFERENCES resources(id, space_id) ON DELETE CASCADE,
|
|
FOREIGN KEY (depends_on, space_id) REFERENCES resources(id, space_id) ON DELETE CASCADE
|
|
);
|
|
|
|
-- Credentials inherit the resource's space; cross-tenant assignment impossible at DB layer.
|
|
CREATE TABLE resource_credentials (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
resource_id uuid NOT NULL,
|
|
space_id uuid NOT NULL,
|
|
label text NOT NULL,
|
|
vault_path text NOT NULL,
|
|
kind text,
|
|
notes text,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
FOREIGN KEY (resource_id, space_id) REFERENCES resources(id, space_id) ON DELETE CASCADE
|
|
);
|
|
|
|
-- source_docs: resource_id is NOT NULL (anchors tenancy) and not in repo update field list,
|
|
-- so a source doc cannot be moved to a different resource. Tenancy inherited transitively.
|
|
CREATE TABLE source_docs (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
resource_id uuid NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
|
name text NOT NULL,
|
|
upstream_url text NOT NULL,
|
|
version text,
|
|
format text,
|
|
sync_source text,
|
|
local_path text,
|
|
body_text text,
|
|
embedding vector(1024),
|
|
last_synced timestamptz,
|
|
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_resources_space ON resources(space_id);
|
|
CREATE INDEX idx_resource_deps_space ON resource_dependencies(space_id);
|
|
CREATE INDEX idx_resource_creds_resource ON resource_credentials(resource_id, space_id);
|
|
CREATE INDEX idx_source_docs_resource ON source_docs(resource_id);
|
|
CREATE INDEX idx_source_docs_fts ON source_docs
|
|
USING GIN (to_tsvector('english', coalesce(body_text,'')));
|
|
CREATE INDEX idx_source_docs_embed ON source_docs
|
|
USING hnsw (embedding vector_cosine_ops);
|