fix: create workspace_entities/workspace_documents registry and provision Workspace rows - #1503
Conversation
…space rows Workspace/Document (workspace_entities/workspace_documents) have been declared in db/models.py since June, but no Alembic migration ever created them explicitly, and no production write path ever inserted a Workspace row. A database that incrementally migrated forward before these models existed never gets the tables (0001's Base.metadata.create_all only reflects today's model metadata, not a historical snapshot), and even where the tables exist, /api/data/documents' Document inserts always violated the workspace_id foreign key since nothing ever created the referenced Workspace row for a real signed session. - Add 0018_workspace_registry.py: idempotent (has_table-guarded) creation of both tables, matching the current model shape and this repo's structured-migration convention. - Add services/workspace_scope.get_or_create_workspace and wire it into both Document-creating endpoints in api/data.py, keyed by the signed session's real workspace claim (workspace-<organization_id>, confirmed against every other call site that derives it) rather than the model's own opaque uuid default. - Fix two unrelated, independently-discovered bugs blocking the documented Alembic path (scripts/migrate_db.py) from ever completing on a genuinely fresh database: schema_backfill_sql() and 0011_email_read_state.py both still targeted the "emails" table, renamed to "email_records" by 0011_email_model_reconciliation long ago. - Add test_workspace_document_migration.py: runs the real scripts/migrate_db.py against a disposable Postgres database (never create_all) and proves /api/data/documents serves cleanly both from an empty database and from one that had already migrated past the point where the registry tables would otherwise be missing. Verified locally against a real PostgreSQL 16 instance: the full Alembic chain now runs 0001->head cleanly from empty, and the full backend test suite passes (1836 passed; the 2 remaining failures are a pre-existing, unrelated is_read NOT NULL smoke-test bug, confirmed present on unmodified develop before this change). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ErwZSYW3pm585NiM3Q7aN
📝 WalkthroughWalkthroughThe pull request hardens Alembic migrations, adds workspace registry tables, provisions workspaces during document uploads, and applies organization-aware document filtering with legacy-row handling. It adds unit and PostgreSQL regression coverage for migrations, concurrency, uploads, and access isolation. ChangesSchema and workspace document integrity
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: 🟡 Moderate · up to This PR adds workspace registry tables and provisions workspaces before document creation, but it is not merge-ready until the downgrade path preserves existing organization assignments. A validly signed but mismatched workspace and organization claim pair could also create inconsistent ownership records, while migration tests currently have configuration-handling and status-validation gaps. Sequence Diagram(s)sequenceDiagram
participant DocumentClient
participant DataAPI
participant WorkspaceScope
participant WorkspaceDatabase
DocumentClient->>DataAPI: Upload document
DataAPI->>WorkspaceScope: get_or_create_workspace
WorkspaceScope->>WorkspaceDatabase: Insert workspace with conflict handling
WorkspaceDatabase-->>WorkspaceScope: Created or existing workspace
WorkspaceScope-->>DataAPI: Workspace
DataAPI->>WorkspaceDatabase: Insert document
WorkspaceDatabase-->>DataAPI: Document record
DataAPI-->>DocumentClient: Upload response
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
PR governance metadata gate is not ready for
|
|
Exact-head audit found and fixed a workspace-provisioning concurrency defect at 31b400c. The former SELECT-then-INSERT allowed simultaneous first uploads for one signed workspace to race on the workspace_entities.workspace_id primary key. Provisioning now uses PostgreSQL ON CONFLICT DO NOTHING with RETURNING and selects the winning row only when another transaction created it. Verification: real PostgreSQL migration/API suite 3 passed (fresh install, pre-registry upgrade, and 16 concurrent first uploads); focused Data API 15 passed and 1 skipped; Ruff and git diff --check passed. @coderabbitai review |
|
|
|
Follow-up exact-head audit fix at 37bcd6e: workspace_id alone was used by document lookup and Data repository listing even though workspace resources must also retain organization scope. A valid signed session carrying the same workspace identifier under another organization could therefore read or mutate that organization’s document. Both query paths now require workspace_id plus organization_id; regression coverage uses the same workspace with a different explicit organization and proves 404. Combined focused validation: 18 passed, 1 skipped; real PostgreSQL fresh/upgrade/concurrency coverage included; Ruff and git diff --check passed. @coderabbitai review |
|
|
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
git status --short
git show --no-ext-diff --format=fuller --stat 37bcd6e38dd26c4aaf9af5837d8f920328bcca16
git diff --no-ext-diff 37bcd6e38dd26c4aaf9af5837d8f920328bcca16^ 37bcd6e38dd26c4aaf9af5837d8f920328bcca16 -- backend
printf '\n--- Python file outlines affected by the commit ---\n'
git diff --name-only 37bcd6e38dd26c4aaf9af5837d8f920328bcca16^ 37bcd6e38dd26c4aaf9af5837d8f920328bcca16 -- 'backend/**/*.py' |
while IFS= read -r file; do
[ -n "$file" ] || continue
printf '\n### %s\n' "$file"
ast-grep outline "$file" --items all --view expanded || true
done
printf '\n--- Workspace/document query candidates ---\n'
rg -n -C 4 --glob 'backend/**/*.py' \
'(Document\.workspace_id|workspace_id\s*==|workspace_id\s*=' backendLength of output: 35493 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- Scoped helper and document endpoints ---'
sed -n '2470,2520p' backend/api/data.py
sed -n '3150,3395p' backend/api/data.py
sed -n '3900,3985p' backend/api/data.py
printf '%s\n' '--- Mock session query emulation ---'
sed -n '55,205p' backend/tests/test_data_api.py
printf '%s\n' '--- Changed document-scope regression test and neighboring listing test ---'
sed -n '2520,2740p' backend/tests/test_data_api.py
printf '%s\n' '--- Direct Document query statements in data API ---'
ast-grep run --lang python \
--pattern 'select(Document)' \
backend/api/data.py || true
ast-grep run --lang python \
--pattern 'update(Document)' \
backend/api/data.py || true
ast-grep run --lang python \
--pattern 'delete(Document)' \
backend/api/data.py || true
printf '%s\n' '--- Callers of the scoped document helper ---'
rg -n -C 3 '\b_get_workspace_document\s*\(' backend/api/data.py
printf '%s\n' '--- Document workspace and organization predicates ---'
rg -n -C 3 'Document\.(workspace_id|organization_id)\s*==' backend/api/data.py backend/tests/test_data_api.pyLength of output: 32842 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- Document model and registry migration ---'
rg -n -C 10 'class Document\b|organization_id.*(Column|mapped_column)' backend/db/models.py
sed -n '1,280p' backend/alembic/versions/0018_workspace_registry.py
printf '%s\n' '--- All backend Document query and write candidates ---'
rg -n -C 4 --glob 'backend/**/*.py' \
'(select\(Document\)|update\(Document\)|delete\(Document\)|query\(Document\)|Document\))' \
backend
printf '%s\n' '--- Organization-collision coverage in document and migration tests ---'
rg -n -i -C 5 \
'(other_organization|org-rival|organization_id|quality-surface|workspace-org-acme)' \
backend/tests/test_data_api.py backend/tests/test_workspace_document_migration.py
printf '%s\n' '--- Exact-head test-file changes since the parent commit ---'
git diff --no-ext-diff --check 37bcd6e38dd26c4aaf9af5837d8f920328bcca16^ 37bcd6e38dd26c4aaf9af5837d8f920328bcca16Length of output: 81549
P2 — Test the organization boundary in the repository listing
The test also does not add a document with the same Set
|
CodeRabbit flagged that test_data_quality_surface_includes_workspace_document_assets didn't actually exercise the new Document.organization_id predicate added in 37bcd6e: doc_owned had no organization_id set, and MockAsyncSession treats a None organization_id as matching any query filter (intentionally, to keep older organization_id-less fixtures usable), so the test passed regardless of whether the real query filtered by organization at all. Give doc_owned its real organization_id and add doc_other_org: same workspace_id, different organization_id. Verified this reproduces the gap (reverting the organization_id predicate in get_data_quality_surface turns this red with object_count 2 instead of 1) and passes with the fix restored. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ErwZSYW3pm585NiM3Q7aN
|
Fixed the P2 test-coverage gap CodeRabbit flagged at Verified: reverting that predicate in Generated by Claude Code |
Devin Review correctly identified two real gaps in 0018_workspace_registry: 1. A database that truly never had workspace_entities/workspace_documents (one whose own 0001_initial_control_plane ran before these models existed, and has only applied incremental migrations since) crashes on 0016_document_org_scope with NoSuchTableError, because 0016 calls inspector.get_columns() unconditionally and it sits before 0018 in the chain. My existing regression test only dropped the tables after 0017, which never exercised this because 0001 always recreates them via live create_all for a genuinely fresh test database -- masking the real bug. Reproduced directly (migrate to 0015, drop the tables, continue to head) and confirmed the crash; 0016 is now has_table-guarded like the rest of this repo's idempotent migrations, and the regression test's pre-registry boundary moved from 0017 to 0015 so it actually crosses 0016 with the tables absent. 2. 0018's downgrade unconditionally dropped both tables, including when its own upgrade was a no-op because they already existed -- so a rollback on any database would destroy workspace_documents.document_content (real uploaded content, not rebuildable derived state like most other tables this repo's migrations manage). Made downgrade a documented no-op, matching the same judgment call 0001_initial_control_plane already makes for the same reason. Verified: the reproduction above now completes cleanly end-to-end through /api/data/documents; full backend suite still 1837 passed (same 2 pre-existing unrelated is_read failures), ruff clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ErwZSYW3pm585NiM3Q7aN
|
Thanks, this review caught a real gap. Addressed at Finding 1 (🔴 historical upgrades never reach registry creation) — confirmed and fixed. Reproduced directly against real PostgreSQL: migrated a fresh database to Finding 2 (📝 fresh migration uses current metadata) — already disclosed, both in the PR description and in Finding 3 (🔴 rollback deletes pre-existing workspace data) — fixed. Finding 4 (🟡 legacy organization documents disappear) — investigated, not changing. This can't currently manifest: every Full backend suite after this fix: 1837 passed (same 2 pre-existing unrelated Generated by Claude Code |
test_legacy_document_scope_postgres.py (e05f1b3) reproduced Devin Review's Finding 4 against real PostgreSQL: 0016_document_org_scope left existing workspace_documents.organization_id unbackfilled (NULL), and the strict Document.organization_id == auth_context.organization_id predicate added in 37bcd6e made such a row invisible even to the organization that actually owns its workspace. Add _document_organization_filter: an exact organization_id match, OR a NULL organization_id, but only when the requesting session's own workspace_id/organization_id pairing is the canonical workspace-<organization_id> (or workspace-<user_id>) derivation -- not an internally inconsistent claim. This is what keeps the cross-tenant boundary 37bcd6e added intact: a session whose workspace_id doesn't match what its own organization_id would derive gets the strict, no-NULL-fallback check, so a forged or malformed claim pairing still can't read a same-workspace document under a different organization. Wired into both _get_workspace_document and get_data_quality_surface's Document query. Verified: test_legacy_document_scope_postgres.py now passes (was failing on this branch's previous commit). Full backend suite: 1838 passed (same 2 pre-existing unrelated is_read failures noted earlier in this PR), ruff clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ErwZSYW3pm585NiM3Q7aN
|
Picked up Fixed at Wired into both
Generated by Claude Code |
|
Migration overlap / historical-upgrade finding on current head
PR #1502 already carries the narrower compatibility contract: preserve Please preserve that legacy-table guard rather than the |
PR #1502 (opened in parallel, ~5 minutes before this one) independently diagnosed the same underlying gap from the same root cause -- running the real backend suite against actual PostgreSQL for the first time -- and found something this PR didn't: app-ci.yml's backend job has never had a Postgres services: container, so every @pytest.mark.postgres test (including all the new ones in this PR) has always silently skipped in real CI. It ships the authoritative fix for that, plus its own version of the 0011_email_read_state /bootstrap_db.py fix, with a pinned contract test. Landing two different versions of the same files from two open PRs would conflict. Adopt #1502's exact pattern for the overlapping files instead of this PR's earlier approach: - 0011_email_read_state.py: has_table("emails")-guarded no-op, keeping the original "emails" target, rather than retargeting to "email_records". (0011_email_model_reconciliation's own docstring clarifies no migration ever renamed "emails" to "email_records" for a real managed database -- email_records was the actual table name since inception; "emails" was only ever a stale copy-pasted string. Both approaches are safe in practice, so there's no reason to diverge from the already-tested pattern.) - bootstrap_db.py / 0001_initial_control_plane.py: schema_backfill_sql()'s callers now go through execute_schema_backfill(), which skips the legacy ix_emails_owner_date statement via identity-matching a LEGACY_EMAILS_INDEX sentinel rather than this PR's simpler unconditional deletion. - test_alembic_migrations.py: contract test now asserts execute_schema_backfill, plus #1502's own test_email_read_state_legacy_table_guard_is_reversible pinning the reconciled 0011 file's shape. - test_bootstrap_db.py / test_data_api.py: the 4 raw SQL `INSERT INTO email_records` smoke-seeding call sites now set is_read explicitly (Python-side ORM default only, no DB server default, so real Postgres rejects the omission) -- the exact bug flagged as a follow-up earlier in this PR's own investigation. Re-verified end-to-end: fresh-database migration to head, and the true historical-database reproduction (migrate to 0015, drop the workspace registry tables, continue to head crossing both 0011_email_read_state and 0016_document_org_scope) both still complete cleanly. Full backend suite: 1841 passed, 0 failed, 3 skipped (up from 1838/2 failed -- the last 2 pre-existing failures are now fixed too), ruff clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ErwZSYW3pm585NiM3Q7aN
|
Reconciled at Adopted its exact pattern for the overlapping files instead of my earlier approach:
For what it's worth, checking Re-verified end-to-end against real PostgreSQL: fresh-database migration to head, and the true historical-database reproduction (migrate to Also worth flagging explicitly since it matters for this PR too: #1502's real finding is that Generated by Claude Code |
Devin Review found two real gaps in the has_table("emails")-only pattern
adopted from PR #1502 in the previous commit:
1. A genuinely historical database -- one whose own 0001 ran before
is_read was added to the Email model, so it has email_records without
is_read -- silently never gets the column: has_table("emails") is False
(per 0011_email_model_reconciliation's docstring, no managed database
ever really had a table literally named "emails"), so upgrade() returned
without touching email_records at all. Reproduced directly: migrated to
0009, dropped email_records.is_read to simulate that historical state,
continued to head with the has_table("emails")-only version -- it
completed with no error, but is_read was permanently missing. Confirmed
the same reproduction now correctly adds is_read to email_records.
2. Not idempotent: a legacy "emails" table that already has is_read (e.g.
from a partial/earlier application) made upgrade() crash with a
duplicate-column error, since it only checked has_table before calling
op.add_column. Reproduced directly (manually created an "emails" table
with is_read already present, migrated to head) and confirmed it no
longer crashes.
Now checks both "email_records" (the table that actually matters) and
"emails" (defensive, in case a real one somehow exists), guarded by column
existence via the same _has_column helper this repo's other migrations
already use, so upgrade/downgrade are safely idempotent either way.
This diverges from PR #1502's exact pinned file shape (its
test_email_read_state_legacy_table_guard_is_reversible asserted the
has_table-only version byte-for-byte), so updated this PR's own contract
test to check for the corrected shape instead of matching that exact text.
Worth flagging on #1502 too, since the same gaps apply to its own version
of this file if it hasn't already been fixed there.
Full backend suite: 1841 passed, 0 failed, 3 skipped, ruff clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ErwZSYW3pm585NiM3Q7aN
|
Two real gaps in the pattern adopted from #1502 — fixed at "Existing email databases miss read state" (🔴) — confirmed and fixed. Reproduced directly: migrated a fresh database to "Legacy read-state migration is not idempotent" (🟡) — confirmed and fixed. Reproduced: manually created an Fix now checks both This diverges from #1502's exact pinned file shape for "Raw index DDL bypasses governance" (🔍) — investigated, not changing. Re-verified end-to-end against real PostgreSQL for all three scenarios above. Full backend suite: 1841 passed, 0 failed, 3 skipped, ruff clean. Generated by Claude Code |
Devin Review found the same ownership-ambiguity problem in this migration's downgrade that 0018_workspace_registry's downgrade already had (fixed earlier in this PR): a fresh database's email_records.is_read comes from 0001's live Base.metadata.create_all, not from 0011_email_read_state, so there is no way for downgrade() to tell "this revision added the column" apart from "the baseline already had it" -- and is_read holds real per-message read/unread state, not rebuildable derived data. Reproduced directly against real PostgreSQL: migrated a fresh database to head, then ran alembic downgrade to 0009 -- the previous op.drop_column version silently deleted email_records.is_read and its data. Made downgrade a documented no-op instead, matching the same judgment call already applied to 0001_initial_control_plane and 0018_workspace_registry. Also added backend/tests/test_email_read_state_migration_postgres.py: permanent real-Postgres coverage for all three scenarios this migration must handle (historical email_records missing is_read gets it added; idempotent against a legacy "emails" table that already has it; downgrade does not destroy a fresh database's read state), addressing Devin's separate note that the existing contract test's string-matching assertions can't detect a destructive downgrade or prove idempotence. Confirmed the downgrade test fails red against the reverted (destructive) version before restoring the fix. Full backend suite: 1844 passed, 0 failed, 3 skipped, ruff clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ErwZSYW3pm585NiM3Q7aN
|
"Fresh database downgrades lose read state" (🔴) — confirmed and fixed at "Migration behavior remains untested" (🔍) — addressed. Added Full backend suite: 1844 passed, 0 failed, 3 skipped, ruff clean. Generated by Claude Code |
|
Acknowledged — I'll treat Generated by Claude Code Generated by Claude Code |
…on fix with #1503 Owner's request on this PR (2026-09-02): this branch had accumulated unrelated content over a long, CI-congestion-heavy session. Narrows it back to its stated purpose -- the dependency-root CI-enablement slice (Postgres service container + the minimum fix needed to make the newly real-executing tests pass) -- and converges the overlapping migration fix with #1503's independently-derived, more complete implementation so the two branches don't carry conflicting historical-migration semantics. Moved out entirely (zero diff vs develop now), extracted to #1531: - .github/workflows/{bandit,dependency-review,docker-publish}.yml and app-ci.yml's `pull_request: branches:` removal (stacked-PR base support) - tests/test_stacked_pr_workflow_contract.py - backend/tests/test_release_governance.py's stacked-PR assertions - scripts/ci/pr_governance_gate.sh + scripts/ci/test_pr_governance_gate.sh (CodeRabbit approval-pending + OpenCode fallback governance-gate fixes) Converged with #1503 (adopted its exact implementation, verified byte-identical via diff against its branch): - backend/alembic/versions/0011_email_read_state.py: #1503 independently found and fixed the same relation "emails" does not exist crash, more completely -- guards on column existence (not just table existence) and checks both "email_records" and legacy "emails", vs. this branch's simpler has_table-only guard. Adopting it here means #1503's later rebase onto this PR (once merged) is a clean no-op on this file instead of a conflict between two divergent fixes for the same bug. - backend/scripts/bootstrap_db.py: same convergence for execute_schema_backfill's legacy-index handling. - backend/tests/test_alembic_migrations.py: swapped this branch's test_email_read_state_legacy_table_guard_is_reversible (asserted the old implementation's exact shape) for #1503's test_email_read_state_guards_both_legacy_and_current_table_names, which asserts the now-shared implementation instead. backend/alembic/versions/0001_initial_control_plane.py, backend/tests/test_bootstrap_db.py, and backend/tests/test_data_api.py needed no changes -- diffed identically against #1503's branch already (both PRs independently reached the exact same is_read raw-SQL fix and 0001 refactor while investigating the same real-Postgres failures). Net result: 10 files changed vs. develop (down from 18), matching the "dependency-root CI-enablement slice" scope the owner asked for. Verification (real-Postgres re-run blocked by this sandbox having no usable Docker daemon -- ExpressJS-style hashes/lint/isolated-assertion checks substitute; the point of this very PR is to make GitHub's own CI runners do the authoritative check): - python3 -m py_compile on every touched Python file: clean. - python3 -m ruff check on every touched backend file: all checks passed. - backend/tests/test_alembic_migrations.py's three targeted test functions executed directly (bypassing this sandbox's unrelated conftest.py/cryptography import breakage): all 3 pass. - git diff against develop for every "moved out" file: zero lines, confirming a clean revert. - git diff against #1503's branch for the converged migration/bootstrap files: zero lines, confirming byte-identical adoption. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019YgmaEKnPhrsbSyQBierFH
|
FYI: per the owner's overlap-audit request on Once Claude-Session: https://claude.ai/code/session_019YgmaEKnPhrsbSyQBierFH Generated by Claude Code |
|
The central Re-triggered the previously-failed Generated by Claude Code Generated by Claude Code |
|
Root cause from the job log: the "prepare verdict" step mints a GitHub App installation token at This is a central-repo ( Still treating this branch as read-only per the prior instruction — no commits pushed here. Generated by Claude Code Generated by Claude Code |
|
Update on the While preparing that fix I discovered My delegated fix (which caught-and-handled that stale-token failure rather than eliminating its cause) is now redundant and was closed: The re-triggered Generated by Claude Code Generated by Claude Code |
Trace PR #1503's signed workspace provisioning and real PostgreSQL migration evidence into the product-technical baseline.\n\nAssisted-by: OpenAI Codex <codex@openai.com>
Non-force merge of PR #1565 into migration owner #1503. Exact synchronization exposed the undeclared TestClient dependency previously masked by an extra local package. Preserve both complete deltas and use #1565 as the direct prerequisite, superseding the historical #1502-before-#1503 proposal without closing either lane. Verified merged tree 793aadd with fresh and repeat Alembic upgrade to 0019_email_read_state_repair, 75 strict PostgreSQL/dependency tests, Ruff, and diff checks. Consumer propagation and hosted gates remain unverified. Co-Authored-By: Codex <noreply@openai.com> Signed-off-by: Seongho Bae <me@seonghobae.me>
Preserve historical receipts while recording the real fresh-install failure, supplemented environment defect, and verified non-force #1565 to #1503 owner integration. Retain remaining consumer propagation, Alembic head merge, and conflicting import acceptance work as Proposed. Narrow the earlier redirect-safety claim to observed configuration evidence. Verification: source-linked logs and artifact hashes, 224 provenance tests, clean-lock owner fresh/repeat migrations and 75 strict tests, diff checks, and independent read-only evidence review. No protected merge or release claim. Co-Authored-By: Codex <noreply@openai.com> Signed-off-by: Seongho Bae <me@seonghobae.me>
Merge #1503 normally as the prerequisite to #1468, preserving the unique bootstrap regression assertion and changelog. The owner already carries the is_read fixture delta. Verified merged tree 00e170c with fresh Alembic upgrade and 75 clean-lock strict PostgreSQL tests, Ruff, and diff checks. No delta is discarded or protected merge claimed. Co-Authored-By: Codex <noreply@openai.com> Signed-off-by: Seongho Bae <me@seonghobae.me>
Preserve the unique no-unconditional-legacy-index assertion and both owner histories. Correct the historical-only canonical-index release claim; conditional legacy handling remains inherited from #1503. Exact dependency sync, fresh and repeat migration 0020, and 131 strict PostgreSQL tests pass. Search and migration performance remain Draft acceptance gates in #1572; no protected completion is claimed. Co-authored-by: Codex <noreply@openai.com> Signed-off-by: Seongho Bae <me@seonghobae.me>
Normally integrate migration owner #1503 at 19d5860 without discarding #1317 history. Preserve both migration branches with a no-DDL merge revision and retain owner-scoped mail and graph identities. Record actual bounded-pool, cancellation, lost-connection and signed API evidence in doctoring; keep ADR Proposed and hosted CI prerequisite #1562 explicit. Candidate verification: 242 focused tests passed against fresh and repeated PostgreSQL migrations; no protected merge or release claimed.
Normally merge full migration prerequisite #1503 at 19d5860, including #1565; preserve #1562/#1531/#1554 history and current #1531 base. Record decision and original missing-emails/skip failures in doctoring before commit. No delta discarded, no copied central workflow, no gate bypass. Add hardened task-only DB lifecycle, isolated bootstrap/child settings, actual pytest collection/xfail guard, cancellation-safe scoped cleanup and redacted artifacts. Full candidate: 1871 passed and 2 explicitly unconfigured live API skips. Independent signal tests passed. Revalidate this exact committed head before protected integration.
Root cause and owner boundary
Naruon's persisted Data workspace models declared
workspace_entitiesandworkspace_documents, but incremental Alembic history never created them for databases that had already applied the original bootstrap migration. Document creation also had no production path that provisioned the signed session's workspace row, so its foreign key could fail even where the tables existed.This PR adds an idempotent structured Alembic migration, race-safe PostgreSQL workspace provisioning, and calls that owner service from both document-creating endpoints. It also repairs historical
emailsreferences so the real migration chain targetsemail_records.The signed server-derived
workspace_idremains the authority. No client-supplied workspace identity, cross-workspace lookup, raw SQL DDL, or environment-secret path is added.Exact current verification — 2026-09-05
19d5860bc27e860acba940390f5792721cd99e5e; verified tree793aadd78a7ad2d6033ca6e12a931dfa776b7374.codex/starlette-testclient-dependency@52dfc863d1a5d6e4e80b6366f719dd09f2aa6172.9c1851336fa04bcdc77c1c6e531afdb882583af1and that exact fix(test): install Starlette TestClient dependency #1565 head. Both ancestors and complete deltas are preserved; no force push.uv sync --lockedin the task-owned backend environment, then disposable PostgreSQL 16.15 + pgvector fresh and repeatscripts/migrate_db.py: both exit 0, recorded revision0019_email_read_state_repair.uv run --frozen python -m pytest -q -W error -ra --tb=short tests/test_alembic_migrations.py tests/test_bootstrap_db.py tests/test_data_api.py tests/test_email_read_state_migration_postgres.py tests/test_legacy_document_scope_postgres.py tests/test_workspace_document_migration.py tests/test_container_dependency_pin_contract.py: 75 passed, 0 failed, 0 skipped, 10.39 seconds.pgvector/pgvector@sha256:ccc6e83d6e35e931dc7c5def2022729d5a6c370318d099181995567ff1fb4d6b. Separate localhost port, random test-only credentials, read-only root, tmpfs storage, scoped cleanup verified. Existing databases were untouched.Root-cause repair and changed stack decision
The previous head's 73-test local rerun used an undeclared httpx2 2.5.0 installation. Exact synchronization removed it; the Data API test then failed collection (exit 4) with missing httpx2 and Starlette's deprecation warning under
-W error. That pass was supplemented-local evidence, not clean-lock proof. #1565 already owns the declared dependency and lock correction, so its unchanged delta is inherited here instead of copied or manually installed.The historical #1502-before-#1503 proposal is superseded by #1565 → #1503. #1502 remains the CI-service lane on #1562; it can later inherit this forward migration repair without a reciprocal prerequisite. Neither PR is closed.
Consumer propagation through #1468 → #1427 → #1497 has not happened. #1497's current fresh install still fails on the old
emailstable name. Its eventual integration must preserve the bootstrap tests and join the provenance and workspace Alembic branches with a forward merge revision; do not rewrite existing revision IDs or stamp past a failure.Evidence boundary
The former 63-fast/8-PostgreSQL and 73-test receipts remain historical observations, not current clean-lock gate evidence. This current receipt proves the local committed tree only; fresh current-head hosted PostgreSQL, security, and independent review evidence is still required. API tests using dependency overrides do not prove a deployed signed HTTP path.
Keep Draft while #1565 is open and hosted gates remain unverified. Historical CHANGES_REQUESTED and failed/cancelled central-workflow records are not transferred to, or treated as success on, this new head. No self-approval, dismissal, dummy commit, force push, admin bypass, or gate weakening.