Skip to content

feat: resolve Oracle join-view INSERT targets by column attribution - #61

Merged
takaebato merged 4 commits into
masterfrom
feat/join-view-insert-target
Jul 5, 2026
Merged

feat: resolve Oracle join-view INSERT targets by column attribution#61
takaebato merged 4 commits into
masterfrom
feat/join-view-insert-target

Conversation

@takaebato

@takaebato takaebato commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the remaining inline-view deferral from #58 (design agreed in
discussion): an Oracle join-view INSERT now resolves to its base table by
column attribution, instead of dropping the whole statement.

INSERT INTO (SELECT e.id, e.name FROM emp e JOIN dept d
             ON e.dept_id = d.id WHERE d.active = 1) VALUES (100, 'x')
-- before: everything empty + UnsupportedStatement
-- after:  create=[emp]  read=[emp, dept]
--         writes=[emp.id, emp.name]
--         reads=[emp.dept_id, dept.id, dept.active]  (ON + WHERE, filter)

Attribution rule (composes two existing mechanisms — no new rules)

The row lands in the one relation every projected column agrees on:

projection column attribution precedent
qualified (e.id) its qualifier, resolved against the view's relations — text-only, no catalog multi-table UPDATE's SET t2.col
unqualified (name) the catalog-owner rule (unqualified_write_binding, #59) over the view's relations #59's read-mirrored SET attribution

Any column ambiguous / unresolved / owned by a different relation than its
siblings — or any factor naming a declared CTE (not a base table) — → drop +
flag as before. Real Oracle rejects those shapes too (the INTO columns must
all belong to one key-preserved table); key-preservedness
itself isn't verified — that needs unique-key metadata the catalog doesn't
carry — attribution assumes a valid statement. The classic comma-join
spelling (FROM emp e, dept d WHERE …) resolves identically.

Companion relations: Insert::target_context

The joined tables other than the target (here dept) become scanned context:

  • table reads ✓ (children() now includes the context, so the scan
    surfaces; emp itself keeps the sink rule — it reads because its columns
    are referenced, not via a scan, consistent with the single-table view path)
  • no data feed ✓ — table lineage's feeding_scans walks input only,
    matching the semantics (the join gates which rows are visible; the value
    path is the source)
  • ON + WHERE land in the existing target_predicate (filter reads over the
    full view scope, aliases included)

TableReference's TryFrom<&Insert> stays shape-determined: the gate
admits joins of plain tables, but a join view's base table needs binder
resolution, so the plain identity parse resolves single-table views only
(documented; join → Err).

Edge cases covered on review

  • CTE factors: any view factor naming a declared CTE — target (used to
    fabricate a write to the CTE) or companion (used to surface the CTE name as
    a phantom base-table read) — now flags the whole statement, uniformly in
    the factor loop. A WITH inside the view stays gate-rejected; a top-level
    CTE consumed only by the source still traces through its body (pinned).
  • USING / NATURAL join views resolve, and the USING clause itself adds no
    reads — consistent with the SELECT path, where reads come from reference
    sites (pinned).
  • Self-join views resolve to the one table (identity is right;
    key-preservedness is per-instance and unverifiable without key metadata).
  • Table lineage: the companion never feeds (s → emp only, no
    dept → emp) — pinned at table level.
  • Gate rejections pinned: FROM-less view, derived factor (single and joined),
    wildcard / expression projections, ClickHouse TABLE FUNCTION targets
    (moved code) incl. an unrepresentable function name.
  • For full patch coverage, the gate hands its proof over entirely as
    data
    (InsertTargetView { factors, projection, join_operators, selection }
    — parse-don't-validate): the unreachable defensive arms are deleted, and
    no consumer re-reads the Query, so factors can't be paired with a
    clause it wasn't derived from.

Tests

Qualified projection (catalog-free), comma form, SELECT-source lineage through
the view (s.x → emp.id), straddling columns → flag, unqualified without a
catalog → flag, unqualified with a catalog attributing by owner
(Oracle-cased catalog), table-level companion read + lineage, USING /
self-join / gate-rejection pins, all CTE placements (factor → flag, inside
the view → flag, top-level source-only → traces through the body), and the
#58 flag test narrowed to an undetermined shape. All gates green (fmt /
clippy / test --all / doc); 788 tests, 100% patch coverage on source
lines (llvm-cov ∩ diff).

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

✅ PR title follows the Conventional Commits spec.

@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.83333% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.92%. Comparing base (13f18bd) to head (74bf42d).

Files with missing lines Patch % Lines
sql-insight/src/resolver/binder/statement.rs 94.53% 0 Missing and 7 partials ⚠️
sql-insight/src/reference.rs 98.24% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #61      +/-   ##
==========================================
+ Coverage   94.84%   94.92%   +0.07%     
==========================================
  Files          27       27              
  Lines        5747     5893     +146     
  Branches     5747     5893     +146     
==========================================
+ Hits         5451     5594     +143     
+ Misses        216      212       -4     
- Partials       80       87       +7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@takaebato
takaebato force-pushed the feat/join-view-insert-target branch 2 times, most recently from 54d1c02 to 93d8c6d Compare July 5, 2026 06:56
`INSERT INTO (SELECT e.id, e.name FROM emp e JOIN dept d ON … WHERE …) …`
used to drop + flag; the row lands in whichever relation **every** projected
column attributes to — a qualified column text-only via its qualifier (like a
multi-table UPDATE's `SET t2.col`), an unqualified one by the catalog-owner
rule — so it now resolves: `emp` is the write target with the projection as
its column list, the companion `dept` surfaces as a scanned table read
(carried on a new `Insert::target_context`, which gates visibility but feeds
no data — table lineage ignores it), and the join ON + WHERE are filter reads
over the view's relations (`target_predicate`). The classic comma-join
spelling (`FROM emp e, dept d WHERE …`) resolves the same way.

No single determined target keeps the previous drop + flag: a non-column
projection item, or a column that is ambiguous / unresolved / owned by a
different relation than its siblings — shapes real Oracle rejects too (the
INTO columns must all belong to one key-preserved table; key-preservedness
itself isn't verified — that needs unique-key metadata the catalog doesn't
carry). The shape gate (`insert_target_view`, now returning the `Select`)
admits joins of plain tables only; `TableReference`'s `TryFrom<&Insert>`
stays shape-determined (single-table views only — a join view's base table
needs binder resolution).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@takaebato
takaebato force-pushed the feat/join-view-insert-target branch from 93d8c6d to 8f400af Compare July 5, 2026 07:17
takaebato and others added 3 commits July 5, 2026 16:49
`InsertTargetView` held both the raw `&Select` and `factors` derived from
its FROM — the same information twice, kept consistent only by the gate
being the sole constructor (a consumer pairing `factors` with a clause it
wasn't derived from would silently misbind). Dissolve `select` into what
consumers actually need, all gate-extracted: `projection`, `join_operators`
(the constraint carriers — no relation data, so nothing overlaps `factors`),
and `selection`. ON-vs-USING interpretation stays in the binder (`join_on`);
the binder-side predicate collection collapses to a plain chain over the
extracted fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A declared CTE only disqualifies the view when the *target* attributes to
it: a CTE as a join-view companion resolves (best-effort scan), a top-level
CTE consumed by the source traces through its body into relation lineage,
and a WITH inside the target view stays gate-rejected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A companion factor naming a declared CTE used to bind as a best-effort
base-table scan, surfacing the CTE's name as a phantom table read (and its
column references against that name, instead of through the body like a
CteRef). A CTE factor makes the view not-a-view-over-base-tables, so the
whole statement now flags and drops — the check moves into the factor loop,
covering target and companion uniformly (the post-attribution CTE-target
check is subsumed and removed). Binding the companion as a real CteRef
stays deliberately unbuilt: no engine executes a WITH + inline-view-target
INSERT, so the machinery isn't worth it until one does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@takaebato
takaebato merged commit 64b7a93 into master Jul 5, 2026
15 checks passed
@takaebato
takaebato deleted the feat/join-view-insert-target branch July 5, 2026 08:40
@github-actions github-actions Bot mentioned this pull request Jul 5, 2026
takaebato added a commit that referenced this pull request Jul 5, 2026
## 🤖 New release

* `sql-insight`: 0.3.0 -> 0.4.0 (✓ API compatible changes)
* `sql-insight-cli`: 0.2.1 -> 0.2.2

<details><summary><i><b>Changelog</b></i></summary><p>

## `sql-insight`

<blockquote>

##
[0.4.0](sql-insight-v0.3.0...sql-insight-v0.4.0)
- 2026-07-05

### ⚠️ Breaking Changes

#### attribute unqualified SET targets with the read-side rules
([#59](#59)) by @takaebato

#### upgrade sqlparser to 0.62
([#55](#55)) by @takaebato

### Added

- resolve Oracle join-view INSERT targets by column attribution
([#61](#61)) by @takaebato
- fan out multi-column-alias lineage to every alias
([#60](#60)) by @takaebato
- resolve Oracle inline-view INSERT targets to their base table
([#58](#58)) by @takaebato

### Fixed

- don't surface a ClickHouse ARRAY JOIN operand as a table read
([#57](#57)) by @takaebato

### Other Changes

- changelog breaking-change workflow, version-bump docs, and keywords
([#51](#51)) by @takaebato
- tidy keywords, README versions, and add a version-sync check
([#46](#46)) by @takaebato
</blockquote>

## `sql-insight-cli`

<blockquote>

##
[0.2.2](sql-insight-cli-v0.2.1...sql-insight-cli-v0.2.2)
- 2026-07-05

### Added

- *(cli)* prebuilt binary distribution — cargo binstall, completions,
man, and provenance
([#53](#53)) by @takaebato

### Fixed

- *(deps)* update rust crate clap_mangen to 0.3
([#54](#54)) by
@renovate[bot]

### Other Changes

- changelog breaking-change workflow, version-bump docs, and keywords
([#51](#51)) by @takaebato
- tidy keywords, README versions, and add a version-sync check
([#46](#46)) by @takaebato
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Takahiro Ebato <takahiro.ebato@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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