Skip to content

Type column names as ColumnName across the read path (keystone)#200

Merged
dfa1 merged 1 commit into
mainfrom
columnname-keystone
Jul 5, 2026
Merged

Type column names as ColumnName across the read path (keystone)#200
dfa1 merged 1 commit into
mainfrom
columnname-keystone

Conversation

@dfa1

@dfa1 dfa1 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

What

Column names inside vortex are typed as ColumnName, not raw String. The keystone is DType.Struct.fieldNames()List<ColumnName>; from there the types flow through the whole read path.

Why (the real reason — it's not "just validation")

A Map<String, Array> lies about its own domain. String says "any text at all"; the map actually holds columns of this schema. Narrowing it to Map<ColumnName, Array> makes the type tell the truth: this maps column names to columns — not arbitrary strings.

That's the payoff, and it's structural:

  • Silent key-mismatch bugs become compiler errors. This PR's own calcite regression proves it: zoneStats.get(col) — a Map<String> looked up with a ColumnNamecompiled (because Map.get(Object) erases the key type) and returned null at runtime. With the key typed, that mismatch can't compile. A whole class of "wrong string in a lookup" bugs is now caught at the source.
  • The manufacture-and-discard smell is gone. StructBuilder.field() already validated each name through ColumnName.of() — then build() unwrapped it back to String one line later. The invariant now survives instead of being thrown away.
  • Illegal states unrepresentable. A footgun field name (blank/control) is rejected structurally at ColumnName construction — a footgun-named schema can no longer be built, so the writer's redundant runtime guard is deleted.

Typed downstream for free: VortexReader.columnStats() keys, RowFilter.Column.column, ScanIterator's projection match, and the writer's internal column-keyed maps — each signature now says what it means.

Boundary discipline

Types live inside; strings stay at the external world, which is correct — those are another system's namespace, not vortex column names:

  • User sugar unchanged: structBuilder().field(String), RowFilter.eq(String, …), ScanOptions.columns(String...), writeChunk(Map<String,Object>) — validate at the edge.
  • Framework/IO seams keep String, converting via .value() right at the boundary: Calcite RelDataType names, CSV headers, JDBC metadata, Parquet schemas, CLI/TUI rendered text.

Verification

  • ./mvnw verify — full, all 15 modules incl. the failsafe Rust-interop oracle. Wire output is byte-identical (the file still stores raw name strings), so the oracle passing is the proof this is behavior-preserving.
  • ./mvnw javadoc:javadoc -pl core zero output; DocsConsistencyTest green.

Public-API change (fieldNames(), columnStats(), RowFilter.Column) — 0.13.0-shaped; changelog updated.

🤖 Generated with Claude Code

…#200)

A Map<String, Array> lies about its own domain: String admits any text,
but the map holds columns of this schema. Typing the key — the keystone
being DType.Struct.fieldNames() -> List<ColumnName> — makes the signature
tell the truth and turns whole classes of key-mismatch bugs into compile
errors (this change's own calcite zoneStats.get(col) NPE, a String-map
looked up with a ColumnName, could no longer compile).

Typed throughout vortex's own APIs: fieldNames(), VortexReader.columnStats()
keys, RowFilter.Column.column, ScanIterator projection, the writer's
internal column maps, and the Chunk.put(ColumnName) builder. The builder's
former put(String) sugar was test-only, so it came off.

String stays only at genuine external boundaries — user-input sugar
(field(String), RowFilter.eq(String), ScanOptions.columns(String...),
writeChunk(Map<String,Object>)) and other-system namespaces (Calcite
RelDataType, CSV headers, JDBC/Parquet metadata) — converted via .value()
at the seam. Wire output byte-identical; Rust-interop oracle green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dfa1 dfa1 force-pushed the columnname-keystone branch from c3314e2 to 6058e9b Compare July 5, 2026 21:55
@dfa1 dfa1 merged commit aded670 into main Jul 5, 2026
6 checks passed
@dfa1 dfa1 deleted the columnname-keystone branch July 5, 2026 21:59
dfa1 added a commit that referenced this pull request Jul 6, 2026
Follow-up to the writeChunk(Map<ColumnName, Object>) retype (#201) and
the Chunk.put(ColumnName, ...) builder (#200): doc snippets still showed
String keys and would not compile. DocsConsistencyTest is blind to them
(the .put("x", ...) / writeChunk(Map<...>) forms have no dotted receiver
its regex can match), so this was caught in review, not CI.

- reference.md: writeChunk(Map<String, Object>) -> Map<ColumnName, Object>
- README / tutorial / compatibility: .put("x", ...) -> .put(ColumnName.of("x"), ...)
- tutorial: add the ColumnName import
- JdbcImporter.toChunkMap: drop a pointless ColumnName -> String -> ColumnName
  round-trip (List<ColumnName> names = schema.fieldNames())

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dfa1 added a commit that referenced this pull request Jul 6, 2026
The Chunk builder row still read put(String column, Object data) but
the method has taken ColumnName since #200. A survey of backticked
method-signature claims (prompted by the #202 review) surfaced it;
#202 fixed the fenced .put snippets but missed this reference table row.

Co-Authored-By: Claude Opus 4.8 <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