Summary: On re-sync, syncProjectAssets stamps every asset with project.ID, but for already-existing projects the upsert returns the struct with an unpopulated (uuid.Nil) ID. Assets are written against 00000000-…, violating fk_projects_assets.
Impact: Assets fail to persist when re-syncing existing external projects (first import is fine). 163 occurrences, clustered at sync runs.
Evidence (prod):
ERROR: insert or update on table "assets" violates foreign key constraint "fk_projects_assets"
DETAIL: Key (project_id)=(00000000-0000-0000-0000-000000000000) is not present in table "projects".
All 163 are the zero UUID, all UPDATE "assets" (conflict-update branch).
Root cause:
- Provider projects arrive with no DB id —
upsertProjects sets only OrganizationID.
UpsertSplit upserts on (external_entity_provider_id, external_entity_id); conflict-update (existing) rows don't get their ID populated back, yet are returned in updatedProjects.
syncProjectAssets:306 trusts it: assets[i].ProjectID = project.ID (= uuid.Nil).
Always exactly the zero UUID, only on re-sync — not a race against a real project.
Reproduce:
docker exec -i devguard-postgresql-1 psql -U devguard -d devguard <<'SQL'
BEGIN;
UPDATE assets SET project_id = '00000000-0000-0000-0000-000000000000'
WHERE id = (SELECT id FROM assets LIMIT 1);
ROLLBACK;
SQL
# => ERROR: ... violates foreign key constraint "fk_projects_assets"
# DETAIL: Key (project_id)=(00000000-...0000) is not present in table "projects".
App-level: configure an external provider and run the project sync twice.
Fix: populate IDs for updated projects in UpsertSplit (re-select by external keys, map id back); guard syncProjectAssets to skip when project.ID == uuid.Nil.
Summary: On re-sync,
syncProjectAssetsstamps every asset withproject.ID, but for already-existing projects the upsert returns the struct with an unpopulated (uuid.Nil) ID. Assets are written against00000000-…, violatingfk_projects_assets.Impact: Assets fail to persist when re-syncing existing external projects (first import is fine). 163 occurrences, clustered at sync runs.
Evidence (prod):
All 163 are the zero UUID, all
UPDATE "assets"(conflict-update branch).Root cause:
upsertProjectssets onlyOrganizationID.UpsertSplitupserts on(external_entity_provider_id, external_entity_id); conflict-update (existing) rows don't get theirIDpopulated back, yet are returned inupdatedProjects.syncProjectAssets:306trusts it:assets[i].ProjectID = project.ID(=uuid.Nil).Always exactly the zero UUID, only on re-sync — not a race against a real project.
Reproduce:
App-level: configure an external provider and run the project sync twice.
Fix: populate IDs for updated projects in
UpsertSplit(re-select by external keys, mapidback); guardsyncProjectAssetsto skip whenproject.ID == uuid.Nil.