Rifqi Hafizuddin Claude Opus 4.8 commited on
Commit ·
09857fc
1
Parent(s): 0066161
/fix data_catalog model + CatalogStore to dedorch schema (data → catalog_payload)
Browse filesPython's Catalog ORM still had the old Python-owned shape (user_id PK + `data`
jsonb), so every catalog read ran `SELECT data_catalog.data` and 500'd against
the Go-migrated dedorch table (post-cutover). Surfaced by the `check` skill:
"what data do I have" → check_data/check_knowledge → UndefinedColumnError.
- Reconcile `Catalog` ORM to dedorch: id PK, scope_type, user_id, analysis_id,
catalog_payload, + partial unique indexes (mirrors AnalysisStateRow).
- CatalogStore.get reads catalog_payload WHERE scope_type='user' (matches Go's
catalog.Service); upsert reconciled + marked legacy (Go owns catalog writes).
- REPO_STATUS §8/§12: drift resolved, data_catalog shape, cutover done.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- REPO_STATUS.md +18 -12
- src/catalog/store.py +19 -7
- src/db/postgres/models.py +32 -9
REPO_STATUS.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
**Audience:** teammates onboarding onto the Python repo (`Agentic-Service-Data-Eyond-Catalog`).
|
| 4 |
**Scope:** what the code does **right now** (branch `pr/4`, ticket KM-652). Describes current state only — no roadmap or to-dos.
|
| 5 |
-
**Snapshot date:** 2026-06-25. **Cross-repo update 2026-06-29:** §2/§8/§11/§12 re-verified against
|
| 6 |
the **Go source** (`Orchestrator-Agent-Service`), not its docs. The Go service has moved well past its
|
| 7 |
own (uncommitted, stale) design docs: it now hosts the **dedorch SQL migrations** in-repo and a full
|
| 8 |
**`/api/v1/analyses` + `/api/v1/skills`** REST surface. Go does **not** call Python yet — those skills
|
|
@@ -178,7 +178,7 @@ unless `SKIP_INIT_DB=true`.
|
|
| 178 |
|---|---|---|---|
|
| 179 |
| `users`, `rooms`, `chat_messages`, `message_sources` | base app | chat endpoint, Go | chat history |
|
| 180 |
| `documents`, `databases` | uploads + DB creds (Fernet-encrypted) | Go ingestion | executor cred resolution |
|
| 181 |
-
| `data_catalog`
|
| 182 |
| `langchain_pg_embedding` | PGVector document chunks | Go ingestion | DocumentRetriever |
|
| 183 |
| `report_inputs` *(was `analysis_records`)* | jsonb `AnalysisRecord`, one per slow-path run; **Python-owned** | slow path | ReportGenerator, report readiness |
|
| 184 |
| `analyses` *(dedorch, plural)* | uuid `id`, `user_id`, `analysis_title`, `objective`, `business_questions` jsonb, `status` (active\|inactive), `data_bind`(+`data_bind_version`), `report_id`, `report_collection` — **defined by Go migrations**; `problem_statement`/`problem_validated`/`owner_id` already **dropped** there (`0003`/`0004`) | Go `/api/v1/analyses`; Python state store | gate (no-op), Help, report |
|
|
@@ -186,12 +186,15 @@ unless `SKIP_INIT_DB=true`.
|
|
| 186 |
| `data_sources` *(dedorch)* | per-analysis binding; `reference_id` = catalog source_id; `type ∈ document\|database` | Go `/analyses/{id}/data-bind` (+ Python `/analysis/create`) | structured-flow scoping, report appendix |
|
| 187 |
| `analyses_messages` *(dedorch)* | the analysis chat room (`role ∈ user\|ai`); replaces deprecated `rooms`/`chat_messages` | Go `/analyses/{id}/messages` | Python chat path **not yet migrated here** (§12) |
|
| 188 |
|
| 189 |
-
>
|
| 190 |
-
>
|
| 191 |
-
> `
|
| 192 |
-
>
|
| 193 |
-
>
|
| 194 |
-
>
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
**Catalog shape** (the jsonb in `data_catalog`):
|
| 197 |
`Catalog → Source[ {source_id, source_type ∈ schema|tabular|unstructured, name, location_ref} → Table[ {table_id, name, row_count, foreign_keys[]} → Column[ {column_id, name, data_type, nullable, pii_flag, sample_values|null, stats} ] ] ]`. PII columns have `sample_values: null` so real values never enter prompts.
|
|
@@ -309,8 +312,8 @@ copies disagree with the current code on:
|
|
| 309 |
|
| 310 |
## 12. dedorch migration — current state
|
| 311 |
|
| 312 |
-
The Python DB
|
| 313 |
-
consumer-only). State **re-verified against the Go source 2026-06-29**:
|
| 314 |
|
| 315 |
- **The dedorch migrations now live IN the Go repo** — embedded SQL at
|
| 316 |
`internal/repository/postgres/migrations/0001_create_core_schema.sql … 0004_replace_chat_with_analysis_scope.sql`,
|
|
@@ -325,8 +328,11 @@ consumer-only). State **re-verified against the Go source 2026-06-29**:
|
|
| 325 |
`rooms`/`chat_messages`/`interview_*` tables to `zdeprecated_*`.
|
| 326 |
- **`report_inputs`** (the slow-path structured output, formerly `analysis_records`) stays
|
| 327 |
**Python-owned**; its finalized schema goes to Harry so the dedorch migration creates it post-cutover.
|
| 328 |
-
-
|
| 329 |
-
|
|
|
|
|
|
|
|
|
|
| 330 |
|
| 331 |
**⚠️ Integration gap (verified — the big one).** Go's `/api/v1/analyses` and `/api/v1/skills`
|
| 332 |
(`help` / `report`) are **placeholders that return dummy data** — the `SendMessage` / `GenerateReport`
|
|
|
|
| 2 |
|
| 3 |
**Audience:** teammates onboarding onto the Python repo (`Agentic-Service-Data-Eyond-Catalog`).
|
| 4 |
**Scope:** what the code does **right now** (branch `pr/4`, ticket KM-652). Describes current state only — no roadmap or to-dos.
|
| 5 |
+
**Snapshot date:** 2026-06-25. **Data-layer reconcile 2026-07-01:** §8/§12 updated — dedorch cutover done, `data_catalog` model reconciled. **Cross-repo update 2026-06-29:** §2/§8/§11/§12 re-verified against
|
| 6 |
the **Go source** (`Orchestrator-Agent-Service`), not its docs. The Go service has moved well past its
|
| 7 |
own (uncommitted, stale) design docs: it now hosts the **dedorch SQL migrations** in-repo and a full
|
| 8 |
**`/api/v1/analyses` + `/api/v1/skills`** REST surface. Go does **not** call Python yet — those skills
|
|
|
|
| 178 |
|---|---|---|---|
|
| 179 |
| `users`, `rooms`, `chat_messages`, `message_sources` | base app | chat endpoint, Go | chat history |
|
| 180 |
| `documents`, `databases` | uploads + DB creds (Fernet-encrypted) | Go ingestion | executor cred resolution |
|
| 181 |
+
| `data_catalog` *(dedorch, Go-owned)* | `id` uuid, `scope_type` ('user'\|'analysis'), `user_id`, `analysis_id`, **`catalog_payload`** jsonb (the `Catalog`: Source → Table → Column), schema_version, generated_at, updated_at; partial-unique on `user_id WHERE scope_type='user'` | **Go `catalog.Service`** (all writes: DB/file ingestion) | CatalogReader → CatalogStore (**read-only**), planner, tools |
|
| 182 |
| `langchain_pg_embedding` | PGVector document chunks | Go ingestion | DocumentRetriever |
|
| 183 |
| `report_inputs` *(was `analysis_records`)* | jsonb `AnalysisRecord`, one per slow-path run; **Python-owned** | slow path | ReportGenerator, report readiness |
|
| 184 |
| `analyses` *(dedorch, plural)* | uuid `id`, `user_id`, `analysis_title`, `objective`, `business_questions` jsonb, `status` (active\|inactive), `data_bind`(+`data_bind_version`), `report_id`, `report_collection` — **defined by Go migrations**; `problem_statement`/`problem_validated`/`owner_id` already **dropped** there (`0003`/`0004`) | Go `/api/v1/analyses`; Python state store | gate (no-op), Help, report |
|
|
|
|
| 186 |
| `data_sources` *(dedorch)* | per-analysis binding; `reference_id` = catalog source_id; `type ∈ document\|database` | Go `/analyses/{id}/data-bind` (+ Python `/analysis/create`) | structured-flow scoping, report appendix |
|
| 187 |
| `analyses_messages` *(dedorch)* | the analysis chat room (`role ∈ user\|ai`); replaces deprecated `rooms`/`chat_messages` | Go `/analyses/{id}/messages` | Python chat path **not yet migrated here** (§12) |
|
| 188 |
|
| 189 |
+
> ✅ **Python ORM ↔ dedorch drift — reconciled 2026-07-01.** `AnalysisStateRow` (`analyses`) dropped
|
| 190 |
+
> `problem_statement`/`problem_validated` and added `objective`/`business_questions` (Harry's #3);
|
| 191 |
+
> `data_catalog` was the last stale model. Its `Catalog` ORM (old `user_id`-PK + `data` jsonb) is now
|
| 192 |
+
> the dedorch shape (`id` PK, `scope_type`, **`catalog_payload`**), and `CatalogStore` reads
|
| 193 |
+
> `catalog_payload WHERE scope_type='user'` (matching Go's `catalog.Service`). This closed a **live
|
| 194 |
+
> bug**: the `check` skill / `CatalogReader` still selected the dropped `data_catalog.data` column, so
|
| 195 |
+
> every catalog read 500'd after the cutover ("what data do I have" → *"Sorry, I couldn't look that up:
|
| 196 |
+
> column data_catalog.data does not exist"*). Python's catalog **write** methods (`upsert`/
|
| 197 |
+
> `remove_source`/`StructuredPipeline`) were reconciled but are now **legacy** — Go owns ingestion.
|
| 198 |
|
| 199 |
**Catalog shape** (the jsonb in `data_catalog`):
|
| 200 |
`Catalog → Source[ {source_id, source_type ∈ schema|tabular|unstructured, name, location_ref} → Table[ {table_id, name, row_count, foreign_keys[]} → Column[ {column_id, name, data_type, nullable, pii_flag, sample_values|null, stats} ] ] ]`. PII columns have `sample_values: null` so real values never enter prompts.
|
|
|
|
| 312 |
|
| 313 |
## 12. dedorch migration — current state
|
| 314 |
|
| 315 |
+
The Python DB has moved from `dataeyond` → **dedorch** (cutover 2026-07-01; Go owns dedorch migrations;
|
| 316 |
+
Python is consumer-only). State **re-verified against the Go source 2026-06-29**:
|
| 317 |
|
| 318 |
- **The dedorch migrations now live IN the Go repo** — embedded SQL at
|
| 319 |
`internal/repository/postgres/migrations/0001_create_core_schema.sql … 0004_replace_chat_with_analysis_scope.sql`,
|
|
|
|
| 328 |
`rooms`/`chat_messages`/`interview_*` tables to `zdeprecated_*`.
|
| 329 |
- **`report_inputs`** (the slow-path structured output, formerly `analysis_records`) stays
|
| 330 |
**Python-owned**; its finalized schema goes to Harry so the dedorch migration creates it post-cutover.
|
| 331 |
+
- **Connection-string cutover DONE (2026-07-01).** Python's `postgres_connstring` now points at
|
| 332 |
+
**dedorch** and reads the Go-migrated tables directly. Every ORM model Python reads (`analyses`,
|
| 333 |
+
`data_sources`, `analyses_messages`, `data_catalog`) has been reconciled to its dedorch shape.
|
| 334 |
+
`create_all` still runs (no `SKIP_INIT_DB` yet) but is now harmless — `checkfirst` skips the
|
| 335 |
+
Go-owned tables. `report_inputs` stays Python-created until Harry's migration lands.
|
| 336 |
|
| 337 |
**⚠️ Integration gap (verified — the big one).** Go's `/api/v1/analyses` and `/api/v1/skills`
|
| 338 |
(`help` / `report`) are **placeholders that return dummy data** — the `SendMessage` / `GenerateReport`
|
src/catalog/store.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
| 1 |
-
"""CatalogStore —
|
| 2 |
|
| 3 |
-
Storage shape: one row per
|
| 4 |
-
(user_id
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
from sqlalchemy import case, delete, func, select
|
|
@@ -27,7 +29,10 @@ class CatalogStore:
|
|
| 27 |
async def get(self, user_id: str) -> Catalog | None:
|
| 28 |
async with AsyncSessionLocal() as session:
|
| 29 |
result = await session.execute(
|
| 30 |
-
select(CatalogRow.
|
|
|
|
|
|
|
|
|
|
| 31 |
)
|
| 32 |
row = result.scalar_one_or_none()
|
| 33 |
if row is None:
|
|
@@ -35,22 +40,29 @@ class CatalogStore:
|
|
| 35 |
return Catalog.model_validate(row)
|
| 36 |
|
| 37 |
async def upsert(self, catalog: Catalog) -> None:
|
|
|
|
|
|
|
| 38 |
payload = catalog.model_dump(mode="json")
|
| 39 |
async with AsyncSessionLocal() as session:
|
| 40 |
stmt = insert(CatalogRow).values(
|
|
|
|
| 41 |
user_id=catalog.user_id,
|
| 42 |
-
|
| 43 |
schema_version=catalog.schema_version,
|
| 44 |
generated_at=catalog.generated_at,
|
| 45 |
updated_at=func.now(),
|
| 46 |
)
|
| 47 |
stmt = stmt.on_conflict_do_update(
|
| 48 |
index_elements=[CatalogRow.user_id],
|
|
|
|
| 49 |
set_={
|
| 50 |
-
"
|
| 51 |
"schema_version": stmt.excluded.schema_version,
|
| 52 |
"updated_at": case(
|
| 53 |
-
(
|
|
|
|
|
|
|
|
|
|
| 54 |
else_=CatalogRow.updated_at,
|
| 55 |
),
|
| 56 |
},
|
|
|
|
| 1 |
+
"""CatalogStore — reads the per-user catalog from the dedorch `data_catalog` table.
|
| 2 |
|
| 3 |
+
Storage shape (Go-owned): one row per scope in `data_catalog`
|
| 4 |
+
(id, scope_type, user_id, analysis_id, catalog_payload jsonb, schema_version,
|
| 5 |
+
generated_at, updated_at). Python reads the user-scoped row (scope_type='user');
|
| 6 |
+
Go's `catalog.Service` owns all writes, so `upsert`/`remove_source` are legacy.
|
| 7 |
"""
|
| 8 |
|
| 9 |
from sqlalchemy import case, delete, func, select
|
|
|
|
| 29 |
async def get(self, user_id: str) -> Catalog | None:
|
| 30 |
async with AsyncSessionLocal() as session:
|
| 31 |
result = await session.execute(
|
| 32 |
+
select(CatalogRow.catalog_payload).where(
|
| 33 |
+
CatalogRow.user_id == user_id,
|
| 34 |
+
CatalogRow.scope_type == "user",
|
| 35 |
+
)
|
| 36 |
)
|
| 37 |
row = result.scalar_one_or_none()
|
| 38 |
if row is None:
|
|
|
|
| 40 |
return Catalog.model_validate(row)
|
| 41 |
|
| 42 |
async def upsert(self, catalog: Catalog) -> None:
|
| 43 |
+
# Legacy: Go's catalog.Service owns catalog writes now. Kept working (and
|
| 44 |
+
# reconciled to the dedorch shape) but no longer on any live Python path.
|
| 45 |
payload = catalog.model_dump(mode="json")
|
| 46 |
async with AsyncSessionLocal() as session:
|
| 47 |
stmt = insert(CatalogRow).values(
|
| 48 |
+
scope_type="user",
|
| 49 |
user_id=catalog.user_id,
|
| 50 |
+
catalog_payload=payload,
|
| 51 |
schema_version=catalog.schema_version,
|
| 52 |
generated_at=catalog.generated_at,
|
| 53 |
updated_at=func.now(),
|
| 54 |
)
|
| 55 |
stmt = stmt.on_conflict_do_update(
|
| 56 |
index_elements=[CatalogRow.user_id],
|
| 57 |
+
index_where=CatalogRow.scope_type == "user",
|
| 58 |
set_={
|
| 59 |
+
"catalog_payload": stmt.excluded.catalog_payload,
|
| 60 |
"schema_version": stmt.excluded.schema_version,
|
| 61 |
"updated_at": case(
|
| 62 |
+
(
|
| 63 |
+
stmt.excluded.catalog_payload != CatalogRow.catalog_payload,
|
| 64 |
+
func.now(),
|
| 65 |
+
),
|
| 66 |
else_=CatalogRow.updated_at,
|
| 67 |
),
|
| 68 |
},
|
src/db/postgres/models.py
CHANGED
|
@@ -6,9 +6,11 @@ from sqlalchemy import (
|
|
| 6 |
Column,
|
| 7 |
DateTime,
|
| 8 |
ForeignKey,
|
|
|
|
| 9 |
Integer,
|
| 10 |
String,
|
| 11 |
Text,
|
|
|
|
| 12 |
)
|
| 13 |
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
| 14 |
from sqlalchemy.orm import relationship
|
|
@@ -108,23 +110,44 @@ class DatabaseClient(Base):
|
|
| 108 |
|
| 109 |
|
| 110 |
class Catalog(Base):
|
| 111 |
-
"""
|
| 112 |
|
| 113 |
-
`
|
| 114 |
-
|
| 115 |
-
`
|
|
|
|
|
|
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
|
|
|
| 119 |
"""
|
| 120 |
__tablename__ = "data_catalog"
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
| 124 |
schema_version = Column(String, nullable=False, default="1.0")
|
| 125 |
-
generated_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 126 |
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
class ReportInputRow(Base):
|
| 130 |
"""One row per completed slow-path analysis (the report's source of truth).
|
|
|
|
| 6 |
Column,
|
| 7 |
DateTime,
|
| 8 |
ForeignKey,
|
| 9 |
+
Index,
|
| 10 |
Integer,
|
| 11 |
String,
|
| 12 |
Text,
|
| 13 |
+
text,
|
| 14 |
)
|
| 15 |
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
| 16 |
from sqlalchemy.orm import relationship
|
|
|
|
| 110 |
|
| 111 |
|
| 112 |
class Catalog(Base):
|
| 113 |
+
"""Data catalog — dedorch **`data_catalog`** (Go-owned; reconciled 2026-07-01).
|
| 114 |
|
| 115 |
+
Mirrors Go migration `0001`/`0002`. One jsonb `catalog_payload` per scope:
|
| 116 |
+
`scope_type='user'` rows are keyed by `user_id` (partial unique index),
|
| 117 |
+
`scope_type='analysis'` rows by `analysis_id`. Python is **consumer-only** —
|
| 118 |
+
Go's `catalog.Service` owns all writes (DB/file ingestion); `CatalogStore`
|
| 119 |
+
reads the user-scoped catalog and its write methods are legacy.
|
| 120 |
|
| 121 |
+
`catalog_payload` holds the full Pydantic Catalog (src/catalog/models.py:Catalog)
|
| 122 |
+
serialized via `model_dump(mode="json")`; the read path rehydrates with
|
| 123 |
+
`Catalog.model_validate(...)`. Go writes the same shape (json tags match).
|
| 124 |
"""
|
| 125 |
__tablename__ = "data_catalog"
|
| 126 |
|
| 127 |
+
id = Column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
|
| 128 |
+
scope_type = Column(String, nullable=False, default="user") # 'user' | 'analysis'
|
| 129 |
+
user_id = Column(String, nullable=False, index=True)
|
| 130 |
+
analysis_id = Column(UUID(as_uuid=False), nullable=True)
|
| 131 |
+
catalog_payload = Column(JSONB, nullable=False)
|
| 132 |
schema_version = Column(String, nullable=False, default="1.0")
|
| 133 |
+
generated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
|
| 134 |
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
| 135 |
|
| 136 |
+
__table_args__ = (
|
| 137 |
+
Index(
|
| 138 |
+
"idx_data_catalog_user_scope",
|
| 139 |
+
"user_id",
|
| 140 |
+
unique=True,
|
| 141 |
+
postgresql_where=text("scope_type = 'user'"),
|
| 142 |
+
),
|
| 143 |
+
Index(
|
| 144 |
+
"idx_data_catalog_analysis_scope",
|
| 145 |
+
"analysis_id",
|
| 146 |
+
unique=True,
|
| 147 |
+
postgresql_where=text("scope_type = 'analysis'"),
|
| 148 |
+
),
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
|
| 152 |
class ReportInputRow(Base):
|
| 153 |
"""One row per completed slow-path analysis (the report's source of truth).
|