Skip to content

fix(knowledge): the code↔DB map invented 33 of the 39 tables it named - #234

Merged
sshlg merged 2 commits into
mainfrom
fix/code-db-link-stops-inventing-tables
Aug 26, 2026
Merged

fix(knowledge): the code↔DB map invented 33 of the 39 tables it named#234
sshlg merged 2 commits into
mainfrom
fix/code-db-link-stops-inventing-tables

Conversation

@sshlg

@sshlg sshlg commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

This is the product's core promise for a repository added to a dashboard: the agent goes to the code and knows how it uses the data. Measured in production on 2026-08-26 against the one real customer project — a Laravel repository against a 213-table MySQL database — the map named 39 tables, of which six exist.

real   catalog_phone_numbers  purchases  unlimit_wallets  sentry_samplers
       coverage_rule_import_exceptions  partner_phone_number_tests

noise  the to if an a get one ever every group its ends cascade
       current_timestamp 31 any ases whatever
       bs ls ns os pts res rs ses sts us xts zes

All three rows surfaced as sync_status='mismatch' — the headline signal, your code says one thing and your database says another — were noise: tables any, ases, whatever, columns e and now.

A customer opening that screen was told their code used a table called whatever.

Two mechanisms, compounding

TABLE_REF_SQL matches FROM|JOIN|INTO|UPDATE|TABLE followed by a word — and prose does that ("update to the latest", "insert into if needed"), as do keywords that legitimately follow (ON DELETE CASCADE). Six SQL keywords were the only filter.

Then _model_name_to_table pluralised whatever it was handed: zzes, sses, ptpts. Every one of the twelve suffix fragments is that function applied to a one- or two-character token — and it was handed them because _extract_model_names takes every class \w+ in a non-Python file while ORM_PATTERNS["sqlalchemy"] matches the bare word Column anywhere, so generated PHP reached the ORM branch and each of its classes became a "model".

The guard

is_plausible_table_name() on every recording site. Four shape rules — length ≥ 3, starts with a letter, not a keyword or common word, not a bare plural suffix on a fragment or a keyword: ases is as pluralised, ends is end.

Every reading is checked, not the first that matches. ends parses as en+ds and as end+s; taking whichever ending matched first let it through, and that was a real bug in my first attempt.

Deliberately shape, not a blocklist — a list of the 33 words that appeared would pass that repository and fail the next.

Measured on the production shapes:

SQL path    raw: 31 catalog_phone_numbers ever every group if purchases
                 sentry_samplers to unlimit_wallets
SQL path   kept: catalog_phone_numbers purchases sentry_samplers unlimit_wallets

pluraliser before: zes ses es us pts res sts xts bs ls ns os rs xes
pluraliser  after: [] — none invented

Second half: a migration states its table

The previous migration parser found nothing in a Laravel repository. It was (?:create_table|CreateTable|CREATE TABLE)\s+['"]?(\w+)— whitespace required before the name — so it missedSchema::create(entirely, missedop.create_table("x")because a paren is not whitespace, and missedcreate_table :xbecause:is not\w`. The map fell back to inference for every table the app has.

eloquent was absent from ORM_PATTERNS. Laravel reached the ORM branch only by accident. It is a first-class entry now, and protected $table = '…' outranks the pluralised class name for the same reason __tablename__ does.

tables_declared_in_migration() reads Laravel, Rails, Alembic, raw DDL and Django. A declaration carries more authority than a guess, which makes a false one worse — so its output passes the same shape test.

Tests

36 across two files, every fixture a real name from that repository rather than an invented one — including the six that were right, which the guard must not remove.

Suite: 6912 passed, 4 skipped, 1 xfailed.

Note on CI: main is unprotected and GitHub has not dispatched a workflow since 2026-08-26T00:30Z (audit findings A-6 / A-7), so this PR may show no checks through no fault of its own. The suite above was run locally in full.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XtEZboEDbEXLYrLf1owRXB

NGHTBOY and others added 2 commits August 27, 2026 00:56
This is the product's core promise for a repository added to a dashboard: the agent goes
to the code and knows how it uses the data. Measured in production on 2026-08-26 against
the one real customer project — a Laravel repository, 213-table MySQL — the map named 39
tables, of which SIX exist.

  real   catalog_phone_numbers purchases unlimit_wallets sentry_samplers
         coverage_rule_import_exceptions partner_phone_number_tests
  noise  the to if an a get one ever every group its ends cascade
         current_timestamp 31 any ases whatever
         bs ls ns os pts res rs ses sts us xts zes

All three rows surfaced as sync_status='mismatch' — the headline signal, "your code says
one thing and your database says another" — were noise: tables `any`, `ases`, `whatever`,
columns `e` and `now`. A customer opening that screen was told their code used a table
called `whatever`.

Two mechanisms, compounding. TABLE_REF_SQL matches FROM|JOIN|INTO|UPDATE|TABLE followed
by a word, and prose does that ("update TO the latest", "insert INTO IF needed"), as do
keywords that legitimately follow (ON DELETE CASCADE); six SQL keywords were the only
filter. Then `_model_name_to_table` pluralised whatever it was handed — z→zes, s→ses,
pt→pts — and it was handed fragments because `_extract_model_names` takes EVERY
`class \w+` in a non-Python file while ORM_PATTERNS["sqlalchemy"] matches the bare word
`Column` anywhere, so generated PHP reached the ORM branch and each class became a model.

- is_plausible_table_name() guards every recording site. Four shape rules; `ases` is `as`
  pluralised and `ends` is `end`, so EVERY reading is checked rather than the first that
  matches — `ends` parses as en+ds and as end+s, and taking the first let it through
- shape, not a blocklist: a list of the 33 observed words would pass that repository and
  fail the next
- `_model_name_to_table` returns "" rather than a guess it cannot stand behind
- measured on the production shapes: the SQL path keeps 4 of 10 and they are exactly the
  four real ones; the pluraliser invents 0 of the 14 it used to

Second half — a migration STATES its table:

- the previous parser found NOTHING in a Laravel repository. It required whitespace
  before the name, so it missed `Schema::create(`, missed `op.create_table("x")` because
  a paren is not whitespace, and missed `create_table :x` because `:` is not `\w`
- `eloquent` was absent from ORM_PATTERNS entirely; Laravel reached the ORM branch only
  by accident. It is first-class now, and `protected $table` outranks the plural for the
  same reason `__tablename__` does
- a declaration carries more authority than a guess, which makes a false declaration
  worse — so migration output passes the same shape test

36 tests across two files, every fixture a real name from that repository.
Suite: 6912 passed, 4 skipped, 1 xfailed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XtEZboEDbEXLYrLf1owRXB
@sshlg
sshlg merged commit edd3ed3 into main Aug 26, 2026
2 checks passed
@sshlg
sshlg deleted the fix/code-db-link-stops-inventing-tables branch August 26, 2026 23:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant