Skip to content

fix(memory): skip non-dict columns in schema_indexer - #2515

Closed
Bartok9 wants to merge 5 commits into
Canner:mainfrom
Bartok9:fix/schema-indexer-skip-non-dict-columns
Closed

fix(memory): skip non-dict columns in schema_indexer#2515
Bartok9 wants to merge 5 commits into
Canner:mainfrom
Bartok9:fix/schema-indexer-skip-non-dict-columns

Conversation

@Bartok9

@Bartok9 Bartok9 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

  • describe_schema / extract_schema_items / _model_record skip non-dict or nameless column entries instead of TypeError.
  • Apache-2.0: core/wren/**.

Motivation

Hand-edited MDL manifests can contain null column slots. Schema indexing should tolerate them and still index valid columns.

Verification

  • cd core/wren && .venv/bin/python -m pytest tests/unit/test_schema_indexer_malformed_columns.py -v2 passed

Real behavior proof

Both tests PASSED (describe + extract paths).

Summary by CodeRabbit

  • Bug Fixes
    • Hardened schema indexing and schema description to tolerate malformed manifest entries by skipping invalid models, columns, relationships, and views.
    • Ensured schema output omits nameless or non-dictionary column content, including removing the “Columns:” section when no valid columns exist.
  • Tests
    • Added unit tests covering null/invalid schema components and verifying only valid named columns contribute to extracted schema items and descriptions.

@github-actions github-actions Bot added python Pull requests that update Python code core labels Jul 16, 2026
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Schema indexing now skips malformed or unnamed manifest entries during schema description and item extraction, with unit tests covering invalid models, views, relationships, and columns.

Changes

Schema Column Hardening

Layer / File(s) Summary
Filter malformed schema entries
core/wren/src/wren/memory/schema_indexer.py
Description and extraction paths skip non-dictionary or unnamed models, views, relationships, and columns; model column summaries use only valid named columns.
Test malformed schema behavior
core/wren/tests/unit/test_schema_indexer_malformed_columns.py
Tests cover malformed entries, omitted column headers, null manifest slots, and preservation of valid model and column records.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: goldmedal

Poem

I’m a rabbit with schemas to mend,
Crooked entries skip round the bend.
Named ones hop into records bright,
Null ones vanish out of sight.
Tests thump their paws: “This flow is right!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is accurate and relevant, though it understates the broader null-slot handling added across schema_indexer.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
core/wren/src/wren/memory/schema_indexer.py (1)

90-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Filter columns before appending the header to avoid a dangling section.

If the columns list only contains malformed items (e.g., [None]), if cols: will still evaluate to true. This results in the " Columns:" header being appended to the schema description, followed by nothing because all items are skipped in the loop.

Consider pre-filtering the columns just like in _model_record so that the header is only added when valid columns exist.

♻️ Proposed refactor
-    cols = model.get("columns", [])
+    cols = [c for c in (model.get("columns") or []) if isinstance(c, dict) and c.get("name")]
     if cols:
         lines.append("  Columns:")
         for col in cols:
-            if not isinstance(col, dict):
-                continue
             _describe_column(col, lines)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/wren/src/wren/memory/schema_indexer.py` around lines 90 - 96, Filter the
columns collection in the schema description flow before the `if cols` check,
retaining only dictionary entries as `_model_record` does. Use the filtered
collection for both the header condition and iteration so `"  Columns:"` is
emitted only when at least one valid column exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@core/wren/src/wren/memory/schema_indexer.py`:
- Around line 90-96: Filter the columns collection in the schema description
flow before the `if cols` check, retaining only dictionary entries as
`_model_record` does. Use the filtered collection for both the header condition
and iteration so `"  Columns:"` is emitted only when at least one valid column
exists.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e823f201-d6d6-4b5e-bd0a-7537cb28ed05

📥 Commits

Reviewing files that changed from the base of the PR and between 82ad7a1 and 4e99347.

📒 Files selected for processing (2)
  • core/wren/src/wren/memory/schema_indexer.py
  • core/wren/tests/unit/test_schema_indexer_malformed_columns.py

CI lint failed on ruff format --check for schema_indexer.py.
Address CodeRabbit nitpick: filter columns before appending the
'Columns:' header so a list of only malformed entries doesn't emit a
dangling header. Mirrors _model_record's filtering approach.
@Bartok9

Bartok9 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai — applied the pre-filter so the Columns: header is only emitted when at least one valid column exists, matching _model_record. Tests still green.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@Bartok9 Thanks for the update — glad the pre-filter now keeps the Columns: header consistent with _model_record, and that tests are still passing.

(๑˃̵ᴗ˂̵)و

@goldmedal

Copy link
Copy Markdown
Collaborator

Nice, minimally-invasive fix and the direction is right. CI is green (wren-ci.yml fires on core/wren/** → lint / unit / memory / mcp / mysql / postgres / ui), and there's no public API change — describe_schema / extract_schema_items keep their signatures, so only previously-crashing input behaves differently.

I verified the behaviour by running the PR head directly. A few actionable items before merge:

1. The raise in _column_record is unreachable, and encodes the opposite contract from the rest of the PR

core/wren/src/wren/memory/schema_indexer.py:308-310

name = col.get("name") or ""
if not name:
    raise ValueError("column record requires name")
  • or "" is redundant — the very next line tests for falsy.
  • _column_record is module-private with exactly one caller (extract_schema_items:236-239), and that caller now continues on not col.get("name") before calling it. So the branch is unreachable through any path in the repo, and no test can cover it. It only fires if you call the private helper directly:
    ValueError: column record requires name
    
  • Semantically it also cuts against the PR's goal: everywhere else a nameless column becomes a skip, here the same condition becomes a hard failure — and it's inconsistent with the sibling builders _model_record / _view_record / _relationship_record, which all still use x["name"].

Suggest dropping the guard and restoring name = col["name"]; the caller already guarantees it.

(Same applies to the early return in _describe_column:98-100 — also unreachable now that _describe_model pre-filters. That one is a plain return rather than a raise, so keeping it as defence-in-depth is defensible.)

2. The fix is column-only — null models / relationships / views slots still raise

The motivation says "hand-edited MDL manifests can contain null slots", but on the PR head:

describe_schema({"models": [None]})           → TypeError: 'NoneType' object is not subscriptable
extract_schema_items({"models": [None]})      → TypeError
describe_schema({"models": [{"columns": []}]})→ KeyError: 'name'
describe_schema({"relationships": [None]})    → TypeError
describe_schema({"views": [None]})            → TypeError

Worth noting this file already has the full isinstance treatment for cubes (:62-66, :242-257), so the defensive pattern is established here — it just isn't extended to models / relationships / views.

Narrowing the scope is fine, but the PR description reads as broader resilience than what's delivered. Either extend the same guard to those three loops (~6 lines), or state the column-level scope explicitly in the description.

3. The behaviour added in 589e0bb has no test

That commit exists solely to suppress the dangling " Columns:" header when every column is malformed — but both new tests include a valid amount column, so the header is emitted in both and the new code path is never exercised.

The behaviour is correct, it's just uncovered:

describe_schema({"models": [{"name": "orders", "columns": [None, {"type": "x"}]}]})
# → '### Model: orders\n'   (no dangling header)

Please add exactly that as a test case.

4. assert "None" not in text is a weak assertion

tests/unit/test_schema_indexer_malformed_columns.py:20 is a whole-document substring check: it doesn't assert which entries were skipped, and it would spuriously fail on a legitimately-named column containing None as a substring. Something precise like assert text.count(" - ") == 1 locks in the intent better.

5. Cheap extra cases worth locking in

All three already behave correctly on this branch, so they're free regression guards:

input current behaviour
{"name": ""} (empty-string name) correctly skipped
columns: null handled, no crash
non-dict scalar, e.g. "amount" correctly skipped

6. Please squash on merge

Two of the four commits are style: ruff-format fixups, and style isn't in the project's conventional-commit set (feat / fix / chore / refactor / test / docs / perf / deps). The PR title is fix(memory): … and the title validator passed, so a squash merge is fine — but a rebase merge would put style: commits in front of release-please.

Unrelated, but noticed while checking: the lint job only runs ruff format --check src/ and ruff check src/, so files under tests/ aren't format-checked.


No blockers. I'd consider 1 and 3 worth doing before merge; 2 just needs a call on whether to widen the fix or narrow the description.

…tests

- extend isinstance/name guards to models/relationships/views in
  describe_schema and extract_schema_items (null slots no longer crash)
- drop unreachable ValueError in _column_record; caller guarantees name
- add tests: dangling Columns header, empty-string/null-column, null
  model/rel/view slots, scalar columns; tighten weak substring assertion
@Bartok9

Bartok9 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough pass @goldmedal — all addressed in 8730311:

  1. Unreachable raise — dropped; restored name = col["name"] since extract_schema_items already guarantees a truthy name. Kept the defence-in-depth early return in _describe_column.
  2. Null model/rel/view slots — widened the guard: describe_schema and extract_schema_items now skip non-dict (and nameless model/view) slots for models, relationships, and views, matching the cube treatment. No more TypeError/KeyError on [None] or [{"columns": []}].
  3. Dangling Columns: header — added test_describe_schema_omits_dangling_columns_header covering the all-malformed case from 589e0bb.
  4. Weak assertion — replaced assert "None" not in text with assert text.count(" - ") == 1.
  5. Extra regression guards — empty-string name, columns: null, and scalar-column cases now locked in.
  6. Squash on merge — will do; happy for you to squash so the style: fixups don't front release-please.

CI green, tests still pass (7 in the malformed-columns module).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
core/wren/tests/unit/test_schema_indexer_malformed_columns.py (1)

47-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover nameless relationship/view entries, not only null slots.

This test does not catch {} or {"name": ""} entries, and it does not exercise extract_schema_items(). Add those cases and assert that no relationship/view sections or records are produced.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/wren/tests/unit/test_schema_indexer_malformed_columns.py` around lines
47 - 50, Add coverage in
test_describe_schema_skips_null_model_relationship_view_slots for nameless
relationship and view entries, including {} and {"name": ""}, and exercise
extract_schema_items() directly. Assert these entries produce no relationship or
view sections or records, while preserving the existing null-slot coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/wren/src/wren/memory/schema_indexer.py`:
- Around line 57-59: Update the relationship and view processing paths to
require each dictionary entry to contain a truthy name before invoking
_describe_relationship, the corresponding view helper, or record builders. Skip
empty dictionaries and entries with blank or missing names while preserving
existing handling for valid named entries. Add regression coverage for nameless
relationships and views.

---

Nitpick comments:
In `@core/wren/tests/unit/test_schema_indexer_malformed_columns.py`:
- Around line 47-50: Add coverage in
test_describe_schema_skips_null_model_relationship_view_slots for nameless
relationship and view entries, including {} and {"name": ""}, and exercise
extract_schema_items() directly. Assert these entries produce no relationship or
view sections or records, while preserving the existing null-slot coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b61c236d-21f7-43bf-92e5-9ede90f6dff2

📥 Commits

Reviewing files that changed from the base of the PR and between 0d07541 and 8730311.

📒 Files selected for processing (2)
  • core/wren/src/wren/memory/schema_indexer.py
  • core/wren/tests/unit/test_schema_indexer_malformed_columns.py

Comment on lines 57 to +59
for rel in manifest.get("relationships", []):
_describe_relationship(rel, lines)
if isinstance(rel, dict):
_describe_relationship(rel, lines)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Skip nameless relationships and views in both paths.

These guards reject non-dictionaries but still pass {} or {"name": ""} to the relationship/view helpers and record builders, contrary to the PR’s stated contract. Require a truthy name here, and add regression cases for nameless relationships/views.

Proposed fix
     for rel in manifest.get("relationships", []):
-        if isinstance(rel, dict):
+        if isinstance(rel, dict) and rel.get("name"):
             _describe_relationship(rel, lines)

     for rel in manifest.get("relationships", []):
-        if not isinstance(rel, dict):
+        if not isinstance(rel, dict) or not rel.get("name"):
             continue

     for view in manifest.get("views", []):
-        if not isinstance(view, dict):
+        if not isinstance(view, dict) or not view.get("name"):
             continue

Also applies to: 247-255

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/wren/src/wren/memory/schema_indexer.py` around lines 57 - 59, Update the
relationship and view processing paths to require each dictionary entry to
contain a truthy name before invoking _describe_relationship, the corresponding
view helper, or record builders. Skip empty dictionaries and entries with blank
or missing names while preserving existing handling for valid named entries. Add
regression coverage for nameless relationships and views.

@goldmedal

Copy link
Copy Markdown
Collaborator

Thanks for the follow-up — items 1–6 from the last round all look correctly applied, and CI is green across all 9 checks.

However, I don't think this should merge as-is: it's been superseded by #2533, which merged into main earlier today and is a strict superset of this change.

I compared this branch's head (8730311d) against main directly:

Cases still broken here, already fixed on main:

                                              this PR            main
describe_schema({"relationships":[{}]})       KeyError: 'name'   ''
extract_schema_items({"relationships":[{}]})  KeyError: 'name'   []
extract_schema_items({"views":[{}]})          KeyError: 'name'   []
describe_schema({"models": None})             TypeError          ''

The relationship guard here is isinstance(rel, dict) only, while _describe_relationship / _relationship_record both still do rel["name"] — so a {} relationship crashes exactly as it did before this PR. That's the CodeRabbit nitpick about nameless relationship/view entries, still open. main also adds _iter_section(), which treats a null section as empty and raises a clear ValueError on a wrong-typed one.

No behavioural delta: all 7 tests from this branch pass unmodified against main's implementation.

Rebase risk: this branch is CONFLICTING and 37 commits behind. Both versions edit the same lines with different semantics, so resolving the conflict toward this branch would drop _iter_section and the nameless rel/view guards — a regression on main.

Suggest closing this as superseded, and opening a small follow-up that cherry-picks the one genuinely additive test: test_describe_schema_omits_dangling_columns_header. main behaves correctly there (_describe_model gates the header on the filtered list) but nothing pins it. The other six cases are already covered by test_schema_indexer_non_dict_models.py.

@Bartok9

Bartok9 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Agreed on all counts @goldmedal — thanks for taking the time to diff head-vs-main so carefully. #2533 is the strict superset (adds _iter_section plus the nameless rel/view guards this branch lacks), and since this branch is 37 commits behind and CONFLICTING, forcing a resolution here would regress main. Closing as superseded.

I'll open a small follow-up that cherry-picks the one genuinely additive case — test_describe_schema_omits_dangling_columns_header — so main's correct behaviour there stays pinned. The other six are already covered by test_schema_indexer_non_dict_models.py.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants