From 9d53efc6e129064bdefb8c2d3216636b9c8c459a Mon Sep 17 00:00:00 2001 From: Minas Arustamyan Date: Tue, 5 May 2026 03:15:09 +0200 Subject: [PATCH] fix(schema-v25): drop FK refs from store tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Past migration finalize steps RENAME / DROP COLUMN / ALTER on the `users` table (e.g. _v12_to_v13_finalize, _v13_to_v14_finalize, _v17_to_v18_finalize, the v5 backfill). DuckDB rejects an ALTER on a table that any other table references via FOREIGN KEY, so the new store_entities / user_store_installs / user_plugin_optouts entries — which the self-heal pass writes to _SYSTEM_SCHEMA before the migration ladder runs — broke 6 legacy-migration tests with: Cannot alter entry "users" because there are entries that depend on it Pre-existing convention (see personal_access_tokens at v6) is to omit FK constraints to `users` and validate user existence at the app layer. Sync the three v25 tables with that convention. Same edit in both _SYSTEM_SCHEMA and _V24_TO_V25_MIGRATIONS so fresh installs and upgraded installs land in the same shape. App-level cascade behavior is unchanged: store entity DELETE explicitly deletes user_store_installs rows in app/api/store.py, and the admin grant-deletion hook explicitly deletes user_plugin_optouts rows for the plugin. The dropped FK constraints were defense-in-depth, not the only guard. --- src/db.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/db.py b/src/db.py index a430ccb..d6e5e27 100644 --- a/src/db.py +++ b/src/db.py @@ -449,9 +449,15 @@ CREATE TABLE IF NOT EXISTS claude_md_template ( -- (admin_granted ∖ opt_outs) ∪ store_installs -- -- See src/marketplace_filter.py:resolve_user_marketplace. +-- FK refs to users(id) intentionally omitted (matches the +-- personal_access_tokens / marketplace_registry pattern). DuckDB blocks +-- ALTER on a referenced parent — past finalize steps RENAME / DROP COLUMN +-- on `users`, which would fail if these store tables held FK refs at the +-- time the ladder reaches them. App-level deletes already cascade +-- explicitly (see app/api/store.py + the resource_grant-deletion hook). CREATE TABLE IF NOT EXISTS store_entities ( id VARCHAR PRIMARY KEY, - owner_user_id VARCHAR NOT NULL REFERENCES users(id), + owner_user_id VARCHAR NOT NULL, owner_username VARCHAR NOT NULL, type VARCHAR NOT NULL CHECK (type IN ('skill','agent','plugin')), name VARCHAR NOT NULL, @@ -469,14 +475,14 @@ CREATE TABLE IF NOT EXISTS store_entities ( ); CREATE TABLE IF NOT EXISTS user_store_installs ( - user_id VARCHAR NOT NULL REFERENCES users(id), - entity_id VARCHAR NOT NULL REFERENCES store_entities(id), + user_id VARCHAR NOT NULL, + entity_id VARCHAR NOT NULL, installed_at TIMESTAMP DEFAULT current_timestamp, PRIMARY KEY (user_id, entity_id) ); CREATE TABLE IF NOT EXISTS user_plugin_optouts ( - user_id VARCHAR NOT NULL REFERENCES users(id), + user_id VARCHAR NOT NULL, marketplace_id VARCHAR NOT NULL, plugin_name VARCHAR NOT NULL, opted_out_at TIMESTAMP DEFAULT current_timestamp, @@ -1726,10 +1732,11 @@ _V22_TO_V23_MIGRATIONS = [ # v25: store + opt-out tables backing the /store and /my-ai-stack pages. _V24_TO_V25_MIGRATIONS = [ + # FK refs deliberately omitted — see the matching note in _SYSTEM_SCHEMA. """ CREATE TABLE IF NOT EXISTS store_entities ( id VARCHAR PRIMARY KEY, - owner_user_id VARCHAR NOT NULL REFERENCES users(id), + owner_user_id VARCHAR NOT NULL, owner_username VARCHAR NOT NULL, type VARCHAR NOT NULL CHECK (type IN ('skill','agent','plugin')), name VARCHAR NOT NULL, @@ -1748,15 +1755,15 @@ _V24_TO_V25_MIGRATIONS = [ """, """ CREATE TABLE IF NOT EXISTS user_store_installs ( - user_id VARCHAR NOT NULL REFERENCES users(id), - entity_id VARCHAR NOT NULL REFERENCES store_entities(id), + user_id VARCHAR NOT NULL, + entity_id VARCHAR NOT NULL, installed_at TIMESTAMP DEFAULT current_timestamp, PRIMARY KEY (user_id, entity_id) ) """, """ CREATE TABLE IF NOT EXISTS user_plugin_optouts ( - user_id VARCHAR NOT NULL REFERENCES users(id), + user_id VARCHAR NOT NULL, marketplace_id VARCHAR NOT NULL, plugin_name VARCHAR NOT NULL, opted_out_at TIMESTAMP DEFAULT current_timestamp,