Skip to content

Conversation

@maparent
Copy link
Collaborator

@maparent maparent commented Jan 17, 2026

https://linear.app/discourse-graphs/issue/ENG-1311/unify-conceptaccess-and-contentaccess-tables
Define ContentAccess in term of source_local_id.
It can then be used to gate access to both Content and Concepts.
Note that this is not a foreign key, so delete cascades have to be handled manually.

Summary by CodeRabbit

  • New Features

    • Enhanced access control system for organizing and managing content visibility within shared spaces.
    • Automatic cleanup mechanisms for content references when items are deleted or modified.
  • Improvements

    • Simplified permission model for content and concept visibility based on space membership.

✏️ Tip: You can customize this high-level summary in your review settings.

@linear
Copy link

linear bot commented Jan 17, 2026

@supabase
Copy link

supabase bot commented Jan 17, 2026

Updates to Preview Branch (eng-1311-unify-conceptaccess-and-contentaccess-tables) ↗︎

Deployments Status Updated
Database Sun, 18 Jan 2026 14:44:18 UTC
Services Sun, 18 Jan 2026 14:44:18 UTC
APIs Sun, 18 Jan 2026 14:44:18 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Sun, 18 Jan 2026 14:44:18 UTC
Migrations Sun, 18 Jan 2026 14:44:21 UTC
Seeding Sun, 18 Jan 2026 14:44:21 UTC
Edge Functions Sun, 18 Jan 2026 14:44:22 UTC

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

@maparent maparent changed the base branch from main to eng-1310-database-change-allow-concepts-to-exist-without-content January 17, 2026 21:54
@maparent maparent marked this pull request as draft January 17, 2026 21:56
@maparent
Copy link
Collaborator Author

@CodeRabbit review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 17, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 17, 2026

📝 Walkthrough

Walkthrough

This pull request consolidates access control between Content and Concept entities by removing the ConceptAccess table, restructuring ContentAccess to use space_id and source_local_id identifiers instead of content_id, introducing trigger-based lifecycle management, and creating new views with updated security policies.

Changes

Cohort / File(s) Summary
Database Type Definitions
packages/database/src/dbTypes.ts
Removed ConceptAccess table; restructured ContentAccess (content_id → source_local_id + space_id); updated function signatures for can_view_specific_concept and can_view_specific_content to use source_local_id_ and space_id_; added is_last_local_reference function
Database Migration
packages/database/supabase/migrations/20260117210851_unify_concept_content_access_tables.sql
Dropped ConceptAccess table; extended ContentAccess with space_id and source_local_id; updated composite primary key and indexes; created trigger functions (on_delete_local_reference, on_update_local_reference, on_delete_space_revoke_local_access) and associated triggers on Content, Concept, Document, and Space tables; updated RLS policies for Document, Content, and ContentAccess; added my_contents and my_concepts views
Concept Schema
packages/database/supabase/schemas/concept.sql
Removed ConceptAccess table and related policies; updated concept_select_policy to use can_view_specific_content(space_id, source_local_id); added trigger functions and triggers for local reference lifecycle management; adjusted my_concepts view to use new access logic
Content Schema
packages/database/supabase/schemas/content.sql
Restructured ContentAccess table (content_id removed, space_id and source_local_id added); updated primary key and indexes; modified can_view_specific_content function signature; updated content/document policies to use space_id and source_local_id; replaced content_id-based security checks with space-based checks

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Content as Content Table
    participant Trigger as Delete Trigger
    participant CheckRef as is_last_local_reference()
    participant ContentAcc as ContentAccess Table

    Client->>Content: DELETE Content (space_id, source_local_id)
    Content-->>Trigger: AFTER DELETE event
    Trigger->>CheckRef: is_last_local_reference(space_id, source_local_id)
    CheckRef-->>Trigger: true (no more refs in Content/Concept/Document)
    Trigger->>ContentAcc: DELETE WHERE space_id & source_local_id match
    ContentAcc-->>Client: Access records cleaned up
Loading
sequenceDiagram
    participant Client
    participant Content as Content Table
    participant Trigger as Update Trigger
    participant ContentAcc as ContentAccess Table

    Client->>Content: UPDATE Content (source_local_id or space_id)
    Content-->>Trigger: AFTER UPDATE event
    Trigger->>ContentAcc: DELETE old (space_id, source_local_id) references
    ContentAcc-->>Trigger: Old access records removed
    Trigger-->>Client: Local reference update propagated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main objective of the changeset: unifying the ConceptAccess and ContentAccess tables into a single access control mechanism.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/database/supabase/schemas/content.sql (1)

648-680: Document access via ContentAccess won’t surface in my_documents view.

Line 650 now allows document access via can_view_specific_content, but public.my_documents still filters only on my_space_ids(). This means users granted local-reference access (and document_of_content lookups) won’t see documents unless they’re space members. Consider aligning the view predicate with the new policy.

♻️ Suggested view alignment (outside this range)
-FROM public."Document" WHERE space_id = any(public.my_space_ids());
+FROM public."Document"
+WHERE (
+    space_id = any(public.my_space_ids())
+    OR public.can_view_specific_content(space_id, source_local_id)
+);
Based on learnings, the `my_*` views are the performance-critical path and should stay consistent with RLS behavior.
🤖 Fix all issues with AI agents
In
`@packages/database/supabase/migrations/20260117210851_unify_concept_content_access_tables.sql`:
- Around line 159-171: The trigger function on_delete_space_revoke_local_access
uses a non-existent column OLD.space_id causing failures on Space deletes;
update the DELETE statement in function on_delete_space_revoke_local_access to
use OLD.id (i.e., DELETE FROM public."ContentAccess" WHERE space_id = OLD.id) so
it correctly revokes ContentAccess rows for the deleted Space; no change needed
to the trigger declaration on_delete_space_revoke_access_trigger beyond this
fix.
- Around line 140-158: The trigger function on_update_local_reference uses
NULL-unsafe comparisons (OLD.space_id != NEW.space_id / OLD.source_local_id !=
NEW.source_local_id) so updates that set fields to NULL can yield NULL and skip
cleanup; change the conditional to use NULL-safe IS DISTINCT FROM (e.g. IF
(OLD.space_id IS DISTINCT FROM NEW.space_id OR OLD.source_local_id IS DISTINCT
FROM NEW.source_local_id) AND public.is_last_local_reference(OLD.space_id,
OLD.source_local_id) THEN ...) so the function reliably detects changes
including to/from NULL and still calls public.is_last_local_reference with the
OLD values before deleting from public."ContentAccess"; update the
on_update_local_reference function accordingly and keep the three CREATE TRIGGER
lines unchanged.
- Line 1: Before dropping the ConceptAccess table, migrate its grants into the
unified ContentAccess scheme by inserting rows into public."ContentAccess" using
ca.account_uid joined with public."Concept" c to obtain c.space_id and
c.source_local_id (only when source_local_id IS NOT NULL), and use ON CONFLICT
DO NOTHING to avoid duplicates; then drop public."ConceptAccess" CASCADE.
Reference the ConceptAccess rows as ca and join to Concept (c) on c.id =
ca.concept_id, mapping to ContentAccess(account_uid, space_id, source_local_id).
- Around line 9-21: Before adding the new primary key "ContentAccess_pkey" on
public."ContentAccess" (account_uid, source_local_id, space_id), deduplicate
existing rows that will collapse when you replace content_id with (space_id,
source_local_id): detect duplicates on (account_uid, space_id, source_local_id)
and remove all but one row per tuple in public."ContentAccess" (for example by
keeping the latest/first by ctid or timestamp), then proceed with the UPDATE
from public."Content" and drop content_id and add the NOT NULL constraints and
primary key; ensure the duplicate-check query and the dedupe DELETE target
public."ContentAccess" so the PK creation will not fail.

In `@packages/database/supabase/schemas/concept.sql`:
- Around line 471-483: The trigger function on_delete_space_revoke_local_access
is referencing a non-existent OLD.space_id on deletes from public."Space";
update the function to use OLD.id (the Space primary key) when deleting rows
from public."ContentAccess" (i.e., DELETE FROM public."ContentAccess" WHERE
space_id = OLD.id) so the delete succeeds; keep the function name
on_delete_space_revoke_local_access and the trigger
on_delete_space_revoke_access_trigger unchanged.
- Around line 452-469: The trigger function on_update_local_reference uses
NULL-unsafe comparisons (OLD.space_id != NEW.space_id and OLD.source_local_id !=
NEW.source_local_id) so updates that set those fields to NULL can yield NULL and
skip cleanup; change those comparisons to NULL-safe "IS DISTINCT FROM" (e.g., IF
(OLD.space_id IS DISTINCT FROM NEW.space_id OR OLD.source_local_id IS DISTINCT
FROM NEW.source_local_id) AND public.is_last_local_reference(OLD.space_id,
OLD.source_local_id) THEN ...) so the delete of public."ContentAccess" runs
correctly when values change to/from NULL; keep the rest of the function and
triggers (on_update_content_trigger, on_update_concept_trigger,
on_update_document_trigger) unchanged.

@@ -0,0 +1,171 @@
DROP TABLE public."ConceptAccess" CASCADE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat packages/database/supabase/migrations/20260117210851_unify_concept_content_access_tables.sql

Repository: DiscourseGraphs/discourse-graph

Length of output: 7321


🏁 Script executed:

# Search for ConceptAccess references in SQL files and other parts of codebase
rg "ConceptAccess" --type sql -i

Repository: DiscourseGraphs/discourse-graph

Length of output: 5007


Migrate ConceptAccess grants before dropping the table.

The migration drops ConceptAccess without transferring its data to the unified ContentAccess system. Concept grants must be migrated using the new (account_uid, space_id, source_local_id) keys by joining the Concept table, similar to how ContentAccess data is being updated from the Content table.

Suggested migration
-- Migrate concept access to the unified content access table
INSERT INTO public."ContentAccess"(account_uid, space_id, source_local_id)
SELECT ca.account_uid, c.space_id, c.source_local_id
FROM public."ConceptAccess" ca
JOIN public."Concept" c ON c.id = ca.concept_id
WHERE c.source_local_id IS NOT NULL
ON CONFLICT DO NOTHING;

DROP TABLE public."ConceptAccess" CASCADE;
🤖 Prompt for AI Agents
In
`@packages/database/supabase/migrations/20260117210851_unify_concept_content_access_tables.sql`
at line 1, Before dropping the ConceptAccess table, migrate its grants into the
unified ContentAccess scheme by inserting rows into public."ContentAccess" using
ca.account_uid joined with public."Concept" c to obtain c.space_id and
c.source_local_id (only when source_local_id IS NOT NULL), and use ON CONFLICT
DO NOTHING to avoid duplicates; then drop public."ConceptAccess" CASCADE.
Reference the ConceptAccess rows as ca and join to Concept (c) on c.id =
ca.concept_id, mapping to ContentAccess(account_uid, space_id, source_local_id).

Comment on lines +9 to +21
UPDATE public."ContentAccess" AS ca
SET space_id = ct.space_id, source_local_id = ct.source_local_id
FROM public."Content" AS ct WHERE ct.id = content_id;

ALTER TABLE public."ContentAccess" DROP COLUMN content_id CASCADE;
-- cascades to Content policies, indices, primary key...

ALTER TABLE public."ContentAccess" ALTER COLUMN space_id SET NOT NULL;
ALTER TABLE public."ContentAccess" ALTER COLUMN source_local_id SET NOT NULL;

ALTER TABLE ONLY public."ContentAccess"
ADD CONSTRAINT "ContentAccess_pkey" PRIMARY KEY (account_uid, source_local_id, space_id);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "20260117210851_unify_concept_content_access_tables.sql" -type f

Repository: DiscourseGraphs/discourse-graph

Length of output: 169


🏁 Script executed:

cat -n ./packages/database/supabase/migrations/20260117210851_unify_concept_content_access_tables.sql

Repository: DiscourseGraphs/discourse-graph

Length of output: 8518


🏁 Script executed:

find ./packages/database -name "*.sql" -type f | head -20

Repository: DiscourseGraphs/discourse-graph

Length of output: 1624


🏁 Script executed:

cat -n ./packages/database/supabase/migrations/20250504202930_content_tables.sql

Repository: DiscourseGraphs/discourse-graph

Length of output: 11119


🏁 Script executed:

cat -n ./packages/database/supabase/migrations/20250610201801_create_access_table.sql

Repository: DiscourseGraphs/discourse-graph

Length of output: 2846


🏁 Script executed:

rg "CREATE TABLE.*ContentAccess" --type sql

Repository: DiscourseGraphs/discourse-graph

Length of output: 316


🏁 Script executed:

cat -n ./packages/database/supabase/migrations/20260102140646_content_and_concept_access_tables.sql

Repository: DiscourseGraphs/discourse-graph

Length of output: 10855


🏁 Script executed:

rg "variant" ./packages/database/supabase/migrations/ -t sql

Repository: DiscourseGraphs/discourse-graph

Length of output: 1775


🏁 Script executed:

cat -n ./packages/database/supabase/migrations/20250718131540_content_variant.sql | head -20

Repository: DiscourseGraphs/discourse-graph

Length of output: 951


🏁 Script executed:

cat -n ./packages/database/supabase/migrations/20260117210851_unify_concept_content_access_tables.sql | sed -n '1,30p'

Repository: DiscourseGraphs/discourse-graph

Length of output: 1640


Deduplicate before adding the new ContentAccess PK.

The Content table has a unique constraint on (space_id, source_local_id, variant), allowing multiple content rows with the same (space_id, source_local_id) but different variants. Mapping content_id → (space_id, source_local_id) can collapse multiple access records into duplicate (account_uid, source_local_id, space_id) tuples, causing the primary key constraint at line 20 to fail. Add deduplication before altering the primary key:

Deduplication queries
-- Verify duplicates exist
SELECT account_uid, space_id, source_local_id, count(*) AS dupes
FROM public."ContentAccess"
GROUP BY 1,2,3
HAVING count(*) > 1;

-- Cleanup: keep one row per (account_uid, space_id, source_local_id)
DELETE FROM public."ContentAccess" a
USING public."ContentAccess" b
WHERE a.ctid < b.ctid
  AND a.account_uid = b.account_uid
  AND a.space_id = b.space_id
  AND a.source_local_id = b.source_local_id;
🤖 Prompt for AI Agents
In
`@packages/database/supabase/migrations/20260117210851_unify_concept_content_access_tables.sql`
around lines 9 - 21, Before adding the new primary key "ContentAccess_pkey" on
public."ContentAccess" (account_uid, source_local_id, space_id), deduplicate
existing rows that will collapse when you replace content_id with (space_id,
source_local_id): detect duplicates on (account_uid, space_id, source_local_id)
and remove all but one row per tuple in public."ContentAccess" (for example by
keeping the latest/first by ctid or timestamp), then proceed with the UPDATE
from public."Content" and drop content_id and add the NOT NULL constraints and
primary key; ensure the duplicate-check query and the dedupe DELETE target
public."ContentAccess" so the PK creation will not fail.

Comment on lines +140 to +158
CREATE OR REPLACE FUNCTION on_update_local_reference() RETURNS TRIGGER
STABLE
SET search_path = ''
SECURITY DEFINER
LANGUAGE plpgsql
AS $$
BEGIN
IF (OLD.space_id != NEW.space_id OR OLD.source_local_id != NEW.source_local_id)
AND public.is_last_local_reference(OLD.space_id, OLD.source_local_id) THEN
DELETE FROM public."ContentAccess" WHERE space_id=OLD.space_id AND source_local_id=OLD.source_local_id;
END IF;
RETURN NEW;
END;
$$;

CREATE TRIGGER on_update_content_trigger AFTER UPDATE ON public."Content" FOR EACH ROW EXECUTE FUNCTION public.on_update_local_reference();
CREATE TRIGGER on_update_concept_trigger AFTER UPDATE ON public."Concept" FOR EACH ROW EXECUTE FUNCTION public.on_update_local_reference();
CREATE TRIGGER on_update_document_trigger AFTER UPDATE ON public."Document" FOR EACH ROW EXECUTE FUNCTION public.on_update_local_reference();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use NULL-safe comparisons in on_update_local_reference.

Same issue as in schema: != yields NULL with NULLs, so updates that clear fields won’t clean stale access rows.

🐛 Proposed fix
-    IF (OLD.space_id != NEW.space_id OR OLD.source_local_id != NEW.source_local_id)
+    IF (OLD.space_id IS DISTINCT FROM NEW.space_id
+        OR OLD.source_local_id IS DISTINCT FROM NEW.source_local_id)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE OR REPLACE FUNCTION on_update_local_reference() RETURNS TRIGGER
STABLE
SET search_path = ''
SECURITY DEFINER
LANGUAGE plpgsql
AS $$
BEGIN
IF (OLD.space_id != NEW.space_id OR OLD.source_local_id != NEW.source_local_id)
AND public.is_last_local_reference(OLD.space_id, OLD.source_local_id) THEN
DELETE FROM public."ContentAccess" WHERE space_id=OLD.space_id AND source_local_id=OLD.source_local_id;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER on_update_content_trigger AFTER UPDATE ON public."Content" FOR EACH ROW EXECUTE FUNCTION public.on_update_local_reference();
CREATE TRIGGER on_update_concept_trigger AFTER UPDATE ON public."Concept" FOR EACH ROW EXECUTE FUNCTION public.on_update_local_reference();
CREATE TRIGGER on_update_document_trigger AFTER UPDATE ON public."Document" FOR EACH ROW EXECUTE FUNCTION public.on_update_local_reference();
CREATE OR REPLACE FUNCTION on_update_local_reference() RETURNS TRIGGER
STABLE
SET search_path = ''
SECURITY DEFINER
LANGUAGE plpgsql
AS $$
BEGIN
IF (OLD.space_id IS DISTINCT FROM NEW.space_id
OR OLD.source_local_id IS DISTINCT FROM NEW.source_local_id)
AND public.is_last_local_reference(OLD.space_id, OLD.source_local_id) THEN
DELETE FROM public."ContentAccess" WHERE space_id=OLD.space_id AND source_local_id=OLD.source_local_id;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER on_update_content_trigger AFTER UPDATE ON public."Content" FOR EACH ROW EXECUTE FUNCTION public.on_update_local_reference();
CREATE TRIGGER on_update_concept_trigger AFTER UPDATE ON public."Concept" FOR EACH ROW EXECUTE FUNCTION public.on_update_local_reference();
CREATE TRIGGER on_update_document_trigger AFTER UPDATE ON public."Document" FOR EACH ROW EXECUTE FUNCTION public.on_update_local_reference();
🤖 Prompt for AI Agents
In
`@packages/database/supabase/migrations/20260117210851_unify_concept_content_access_tables.sql`
around lines 140 - 158, The trigger function on_update_local_reference uses
NULL-unsafe comparisons (OLD.space_id != NEW.space_id / OLD.source_local_id !=
NEW.source_local_id) so updates that set fields to NULL can yield NULL and skip
cleanup; change the conditional to use NULL-safe IS DISTINCT FROM (e.g. IF
(OLD.space_id IS DISTINCT FROM NEW.space_id OR OLD.source_local_id IS DISTINCT
FROM NEW.source_local_id) AND public.is_last_local_reference(OLD.space_id,
OLD.source_local_id) THEN ...) so the function reliably detects changes
including to/from NULL and still calls public.is_last_local_reference with the
OLD values before deleting from public."ContentAccess"; update the
on_update_local_reference function accordingly and keep the three CREATE TRIGGER
lines unchanged.

Comment on lines +159 to +171
CREATE OR REPLACE FUNCTION on_delete_space_revoke_local_access() RETURNS TRIGGER
STABLE
SET search_path = ''
SECURITY DEFINER
LANGUAGE plpgsql
AS $$
BEGIN
DELETE FROM public."ContentAccess" WHERE space_id=OLD.space_id;
RETURN OLD;
END;
$$;

CREATE TRIGGER on_delete_space_revoke_access_trigger AFTER DELETE ON public."Space" FOR EACH ROW EXECUTE FUNCTION public.on_delete_space_revoke_local_access();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Space-delete trigger references a non-existent field.

Line 166 should use OLD.id, not OLD.space_id, or the trigger will error on deletes.

🐛 Proposed fix
-    DELETE FROM public."ContentAccess" WHERE space_id=OLD.space_id;
+    DELETE FROM public."ContentAccess" WHERE space_id=OLD.id;
🤖 Prompt for AI Agents
In
`@packages/database/supabase/migrations/20260117210851_unify_concept_content_access_tables.sql`
around lines 159 - 171, The trigger function on_delete_space_revoke_local_access
uses a non-existent column OLD.space_id causing failures on Space deletes;
update the DELETE statement in function on_delete_space_revoke_local_access to
use OLD.id (i.e., DELETE FROM public."ContentAccess" WHERE space_id = OLD.id) so
it correctly revokes ContentAccess rows for the deleted Space; no change needed
to the trigger declaration on_delete_space_revoke_access_trigger beyond this
fix.

Comment on lines +452 to +469
CREATE OR REPLACE FUNCTION on_update_local_reference() RETURNS TRIGGER
STABLE
SET search_path = ''
SECURITY DEFINER
LANGUAGE plpgsql
AS $$
BEGIN
IF (OLD.space_id != NEW.space_id OR OLD.source_local_id != NEW.source_local_id)
AND public.is_last_local_reference(OLD.space_id, OLD.source_local_id) THEN
DELETE FROM public."ContentAccess" WHERE space_id=OLD.space_id AND source_local_id=OLD.source_local_id;
END IF;
RETURN NEW;
END;
$$;

CREATE TRIGGER on_update_content_trigger AFTER UPDATE ON public."Content" FOR EACH ROW EXECUTE FUNCTION public.on_update_local_reference();
CREATE TRIGGER on_update_concept_trigger AFTER UPDATE ON public."Concept" FOR EACH ROW EXECUTE FUNCTION public.on_update_local_reference();
CREATE TRIGGER on_update_document_trigger AFTER UPDATE ON public."Document" FOR EACH ROW EXECUTE FUNCTION public.on_update_local_reference();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use NULL-safe comparisons when deciding cleanup on update.

!= returns NULL if either side is NULL, so updates that clear source_local_id/space_id won’t delete stale ContentAccess rows. Prefer IS DISTINCT FROM.

🐛 Proposed fix
-    IF (OLD.space_id != NEW.space_id OR OLD.source_local_id != NEW.source_local_id)
+    IF (OLD.space_id IS DISTINCT FROM NEW.space_id
+        OR OLD.source_local_id IS DISTINCT FROM NEW.source_local_id)
🤖 Prompt for AI Agents
In `@packages/database/supabase/schemas/concept.sql` around lines 452 - 469, The
trigger function on_update_local_reference uses NULL-unsafe comparisons
(OLD.space_id != NEW.space_id and OLD.source_local_id != NEW.source_local_id) so
updates that set those fields to NULL can yield NULL and skip cleanup; change
those comparisons to NULL-safe "IS DISTINCT FROM" (e.g., IF (OLD.space_id IS
DISTINCT FROM NEW.space_id OR OLD.source_local_id IS DISTINCT FROM
NEW.source_local_id) AND public.is_last_local_reference(OLD.space_id,
OLD.source_local_id) THEN ...) so the delete of public."ContentAccess" runs
correctly when values change to/from NULL; keep the rest of the function and
triggers (on_update_content_trigger, on_update_concept_trigger,
on_update_document_trigger) unchanged.

Comment on lines +471 to +483
CREATE OR REPLACE FUNCTION on_delete_space_revoke_local_access() RETURNS TRIGGER
STABLE
SET search_path = ''
SECURITY DEFINER
LANGUAGE plpgsql
AS $$
BEGIN
DELETE FROM public."ContentAccess" WHERE space_id=OLD.space_id;
RETURN OLD;
END;
$$;

CREATE TRIGGER on_delete_space_revoke_access_trigger AFTER DELETE ON public."Space" FOR EACH ROW EXECUTE FUNCTION public.on_delete_space_revoke_local_access();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Trigger uses a non-existent field on Space deletes.

Line 478 references OLD.space_id, but public."Space" uses id. This will error and block deletes.

🐛 Proposed fix
-    DELETE FROM public."ContentAccess" WHERE space_id=OLD.space_id;
+    DELETE FROM public."ContentAccess" WHERE space_id=OLD.id;
🤖 Prompt for AI Agents
In `@packages/database/supabase/schemas/concept.sql` around lines 471 - 483, The
trigger function on_delete_space_revoke_local_access is referencing a
non-existent OLD.space_id on deletes from public."Space"; update the function to
use OLD.id (the Space primary key) when deleting rows from
public."ContentAccess" (i.e., DELETE FROM public."ContentAccess" WHERE space_id
= OLD.id) so the delete succeeds; keep the function name
on_delete_space_revoke_local_access and the trigger
on_delete_space_revoke_access_trigger unchanged.

@maparent maparent force-pushed the eng-1311-unify-conceptaccess-and-contentaccess-tables branch from 4913e56 to c306427 Compare January 18, 2026 14:43
@maparent maparent force-pushed the eng-1310-database-change-allow-concepts-to-exist-without-content branch from edd277a to b0c533b Compare January 18, 2026 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants