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 files

Python'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>

Files changed (3) hide show
  1. REPO_STATUS.md +18 -12
  2. src/catalog/store.py +19 -7
  3. 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` | per-user jsonb `Catalog` (Source → Table → Column) | Go ingestion / Python pipeline | CatalogReader, 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,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
- > ⚠️ **Python ORM ↔ dedorch drift (verified 2026-06-29).** Python's `AnalysisStateRow` + `state_store.py`
190
- > still model **`problem_statement` / `problem_validated`** and do **not** carry `objective` /
191
- > `business_questions`, but the Go migrations have already dropped the former and added the latter.
192
- > Pre-cutover this is harmless (Python runs `create_all` on its own copy); **post-`SKIP_INIT_DB`**, when
193
- > Python reads dedorch directly, ORM column selection on the dropped columns will break. Reconcile the
194
- > Python model before the connection-string cutover.
 
 
 
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 is moving from `dataeyond` → **dedorch** (Go owns dedorch migrations; Python is
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
- - The connection-string cutover (paired with `SKIP_INIT_DB`) **has not happened yet**; Python still
329
- runs `create_all` on its own models until then.
 
 
 
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 — persists per-user catalogs as Postgres jsonb rows.
2
 
3
- Storage shape: one row per user in a `catalogs` table with columns
4
- (user_id PK, data jsonb, schema_version, generated_at, updated_at).
 
 
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.data).where(CatalogRow.user_id == user_id)
 
 
 
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
- data=payload,
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
- "data": stmt.excluded.data,
51
  "schema_version": stmt.excluded.schema_version,
52
  "updated_at": case(
53
- (stmt.excluded.data != CatalogRow.data, func.now()),
 
 
 
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
- """Per-user data catalog stored as a single jsonb row.
112
 
113
- `data` holds the full Pydantic Catalog (src/catalog/models.py:Catalog)
114
- serialized via `model_dump(mode="json")`. Read path uses
115
- `Catalog.model_validate(...)` to rehydrate.
 
 
116
 
117
- Dedicated table kept separate from `langchain_pg_embedding` so unstructured
118
- embeddings and structured-catalog metadata never share storage.
 
119
  """
120
  __tablename__ = "data_catalog"
121
 
122
- user_id = Column(String, primary_key=True)
123
- data = Column(JSONB, nullable=False)
 
 
 
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).