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.
We decided to rename it to ResourceAccess to express this.
Note that source_local_id is not a foreign key, so delete cascades have to be handled manually.

https://www.loom.com/share/4c29f549c2a549c8a9c50dea11f02564

Summary by CodeRabbit

  • New Features

    • Unified resource access using space + local IDs and automated lifecycle checks.
    • Added a public check to detect when a resource’s last local reference is removed.
  • Improvements

    • Permission checks now consider space membership or explicit local-ID access across contents, concepts, and documents.
    • Views and policies updated for consistent, faster access decisions and indexing.
  • Removals

    • Legacy concept-specific access table and its related policies removed.

✏️ 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 Tue, 20 Jan 2026 02:41:04 UTC
Services Tue, 20 Jan 2026 02:41:04 UTC
APIs Tue, 20 Jan 2026 02:41:04 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 Tue, 20 Jan 2026 02:41:04 UTC
Migrations Tue, 20 Jan 2026 02:41:05 UTC
Seeding Tue, 20 Jan 2026 02:41:05 UTC
Edge Functions Tue, 20 Jan 2026 02:41:06 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

Consolidates concept- and content-specific access into a unified ResourceAccess table keyed by (account_uid, source_local_id, space_id); replaces can_view_specific_concept/content with can_view_specific_resource; adds is_last_local_reference and DB triggers/functions to maintain ResourceAccess and update RLS views/policies.

Changes

Cohort / File(s) Summary
DB Type Definitions
packages/database/src/dbTypes.ts
Removed ConceptAccess; replaced ContentAccess with ResourceAccess (adds space_id, source_local_id); updated access function signatures to can_view_specific_resource(space_id, source_local_id) and added is_last_local_reference
Migration SQL
packages/database/supabase/migrations/20260117210851_unify_concept_content_access_tables.sql
Drops ConceptAccess; renames/rewrites ContentAccessResourceAccess, backfills data, sets PK (account_uid, source_local_id, space_id), adds index; adds can_view_specific_resource, is_last_local_reference, new views (my_documents, my_contents, my_concepts), trigger functions and triggers; updates RLS policies
Concept Schema
packages/database/supabase/schemas/concept.sql
Removes ConceptAccess; replaces can_view_specific_concept(id) checks with can_view_specific_resource(space_id, source_local_id); adds is_last_local_reference, trigger functions and attaches triggers to Concept/Content/Document and Space
Content Schema
packages/database/supabase/schemas/content.sql
Renames/rewrites ContentAccessResourceAccess (drop content_id, add space_id + source_local_id), updates PK/index/comments; replaces can_view_specific_content(id) with can_view_specific_resource(space_id, source_local_id); updates my_documents/my_contents views and RLS policies

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant DB as Database
    participant Trigger as on_delete_local_reference()
    participant Checker as is_last_local_reference()
    participant RA as ResourceAccess

    Client->>DB: DELETE FROM "Content"/"Concept"/"Document" (row with source_local_id, space_id)
    DB-->>Trigger: AFTER DELETE ROW fires
    Trigger->>Checker: is_last_local_reference(space_id, source_local_id)
    Checker-->>Trigger: boolean
    alt last local reference
        Trigger->>RA: DELETE FROM ResourceAccess WHERE source_local_id=... AND space_id=...
    end
    Trigger-->>DB: complete
Loading
sequenceDiagram
    participant Client
    participant DB as Database
    participant Trigger as on_update_local_reference()
    participant RA as ResourceAccess

    Client->>DB: UPDATE "Content"/"Concept"/"Document" (change source_local_id or space_id)
    DB-->>Trigger: AFTER UPDATE ROW fires
    Trigger->>RA: DELETE/UPDATE ResourceAccess entries for OLD (source_local_id, space_id) as needed
    RA-->>Trigger: rows removed/updated
    Trigger-->>DB: complete
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 accurately describes the main objective: unifying ConceptAccess and ContentAccess tables into ResourceAccess, which is the primary change across all modified files.
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.

@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
Base automatically changed from eng-1310-database-change-allow-concepts-to-exist-without-content to main January 19, 2026 04:22
@maparent maparent force-pushed the eng-1311-unify-conceptaccess-and-contentaccess-tables branch from c306427 to 4e8f0a4 Compare January 19, 2026 04:32
@maparent
Copy link
Collaborator Author

@CodeRabbit review last corrections.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

@maparent: I'll review the last corrections made to the pull request.


🧠 Learnings used
Learnt from: maparent
Repo: DiscourseGraphs/discourse-graph PR: 712
File: packages/database/supabase/migrations/20260117210851_unify_concept_content_access_tables.sql:9-21
Timestamp: 2026-01-19T13:56:33.389Z
Learning: At the time of migration 20260117210851_unify_concept_content_access_tables.sql in the discourse-graph project, all ContentAccess records were built from the full variant (including space_id, source_local_id, and variant), ensuring no duplicate (account_uid, space_id, source_local_id) tuples exist when dropping the content_id column.
✅ 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.

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: 3

🤖 Fix all issues with AI agents
In `@packages/database/supabase/schemas/concept.sql`:
- Around line 452-466: The trigger function on_update_local_reference is
declared STABLE but performs a DELETE (data modification); update its volatility
to VOLATILE (or remove the STABLE clause so it uses the default VOLATILE) in the
function declaration to reflect that it changes data (leave SECURITY DEFINER,
search_path, and body as-is); ensure the change is made on the CREATE OR REPLACE
FUNCTION on_update_local_reference() declaration so the trigger behaves
correctly.
- Around line 434-446: The trigger function on_delete_local_reference is
declared STABLE but performs a DELETE; change its volatility to VOLATILE (or
remove the STABLE clause so it defaults to VOLATILE) in the CREATE OR REPLACE
FUNCTION definition; keep the body intact (calls to
public.is_last_local_reference and DELETE FROM public."ContentAccess" remain
unchanged) and ensure SECURITY DEFINER, search_path, and LANGUAGE plpgsql
clauses are preserved.
- Around line 472-484: The function on_delete_space_revoke_local_access is
declared STABLE but performs a DELETE; update its volatility to VOLATILE (or
remove the STABLE clause so default VOLATILE is used) to reflect that it
modifies data, leaving RETURNS TRIGGER, SECURITY DEFINER, and the trigger on
public."Space" unchanged; ensure the function signature
on_delete_space_revoke_local_access() and the trigger declaration still match
after the change.
♻️ Duplicate comments (1)
packages/database/supabase/migrations/20260117210851_unify_concept_content_access_tables.sql (1)

136-186: Trigger functions have the same STABLE volatility issue as in the schema file.

The fixes for NULL-safe comparisons (IS DISTINCT FROM at lines 161-162) and OLD.id (line 181) are correctly applied. The STABLE volatility issue is reported in the schema file. Based on learnings, schema changes should be fixed in subsequent migrations or schema definition files.

🧹 Nitpick comments (1)
packages/database/supabase/schemas/concept.sql (1)

423-432: Consider adding LIMIT 1 to all subqueries for consistency.

The Content subquery has LIMIT 1 but Concept and Document don't. While functionally correct (EXISTS short-circuits), adding LIMIT 1 to all three maintains consistency and makes the optimization intent explicit.

♻️ Suggested consistency fix
 CREATE OR REPLACE FUNCTION public.is_last_local_reference(space_id_ BIGINT, source_local_id_ VARCHAR) RETURNS boolean
 STABLE
 SET search_path = ''
 SECURITY DEFINER
 LANGUAGE sql
 AS $$
     SELECT NOT EXISTS (SELECT id FROM public."Content" WHERE space_id=space_id_ AND source_local_id=source_local_id_ LIMIT 1)
-       AND NOT EXISTS (SELECT id FROM public."Concept" WHERE space_id=space_id_ AND source_local_id=source_local_id_)
-       AND NOT EXISTS (SELECT id FROM public."Document" WHERE space_id=space_id_ AND source_local_id=source_local_id_);
+       AND NOT EXISTS (SELECT id FROM public."Concept" WHERE space_id=space_id_ AND source_local_id=source_local_id_ LIMIT 1)
+       AND NOT EXISTS (SELECT id FROM public."Document" WHERE space_id=space_id_ AND source_local_id=source_local_id_ LIMIT 1);
 $$;

@maparent
Copy link
Collaborator Author

@CodeRabbit review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 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.

@maparent maparent marked this pull request as ready for review January 19, 2026 14:47
@maparent
Copy link
Collaborator Author

@CodeRabbit full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

✅ Actions performed

Full review triggered.

@maparent
Copy link
Collaborator Author

After discussing with Trang, we thought it would be less confusing to rename ContentAccess to ResourceAccess.

@maparent
Copy link
Collaborator Author

@CodeRabbit full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

✅ Actions performed

Full review triggered.

@maparent maparent requested a review from mdroidian January 20, 2026 03:06
@maparent
Copy link
Collaborator Author

@mdroidian giving up on code rabbit.

@mdroidian
Copy link
Contributor

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

✅ Actions performed

Full review triggered.

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.

3 participants