haochunchang commented on issue #25903:
URL: https://github.com/apache/datafusion/issues/25903#issuecomment-5979634968
## Proposed change of direction: attach labels as field metadata instead of
renaming
After digging into open question 1 (renaming or metadata), I'd like to
change the proposal to the metadata approach. Column names stay exactly as they
are today and remain the lookup key. The readable label is attached to each
output field as Arrow field metadata, and tools that display results show the
label instead of the name.
Everything about how a label is **computed** stays as proposed above: the
`label_for` trace through the unoptimized plan, the labeling rules, window
rendering, and the `SqlDisplay` / `UdafHumanDisplayBuilder` fixes. Only the
last step changes: how the label is applied to the result.
### Why
The three hardest parts of the rename design all come from changing the
result's column names:
- Client code that reads columns by the old name breaks (`df["sum(t.a)"]` in
Python, JDBC, ADBC, Flight SQL, Ballista, and DataFrame calls after
`ctx.sql()`).
- Two columns with the same label, such as `SELECT t1.a + 1, t2.a + 1` or
`SELECT 1, '1'`, can't both be renamed, so every collision needs a fallback
rule.
- The label becomes the result's lookup key, so the name still does two jobs.
With metadata, no name changes, so none of these apply. Open questions 1, 3
(SQL followed by DataFrame calls), and 4 (collision handling) go away. The
trade-off is that clients that don't read field metadata, such as pandas, JDBC,
and BI tools, keep showing today's names.
### How labels are attached
The labeling step in the `Statement::Query` branch of
`sql_statement_to_plan` adds a projection that aliases each labeled column **to
its own name** and attaches the label with `Expr::alias_with_metadata`. Each
labeled field carries two keys:
| Key | Value |
| --------------------- | ----------------------------------- |
| `datafusion.label` | The label, such as `sum(a)` |
| `datafusion.label_of` | The column name, such as `sum(t.a)` |
`datafusion.label_of` guards against stale labels. `Alias::to_field`
(`datafusion/expr/src/expr_schema.rs`) merges the inner field's metadata into
an aliased field, so after `df.with_column_renamed("sum(t.a)", "total")` the
field would still carry `sum(a)`. A display tool uses the label only when the
field name equals `datafusion.label_of`, so the renamed column displays `total`.
Rule changes compared with the original proposal:
- A column gets no metadata when its label equals its name, or when the
trace returns `None`. A query in which no column gets a label gets no labeling
projection.
- Explicit aliases and columns named in the outermost `SELECT` get no label,
so they display their names.
- Duplicate labels are allowed. Names stay unique, so the query works as it
does today.
The behavior tables above still apply, with "Proposed name" read as "Label".
The "Current name" column becomes the column name, which doesn't change.
### The label already reaches the result
DataFusion already carries alias metadata from the logical plan to the
record batches:
- `OptimizeProjections` removes a same-name alias only when it has no
metadata (`datafusion/optimizer/src/optimize_projections/mod.rs`).
- `ProjectionExec` takes field metadata from the logical output schema
(`ProjectionExprs::make_projector_with_schema_metadata` in
`datafusion/physical-expr/src/projection.rs`).
- Physical projection-removal rules keep a projection that sets metadata
(`ProjectionExec::overrides_metadata` in
`datafusion/physical-plan/src/projection.rs`).
A local prototype test attaches labels this way on top of the unoptimized
plan for `SELECT a + 1, sum(b) FROM t GROUP BY a ORDER BY a + 1 DESC LIMIT 2`.
Every batch from `collect()` carries the labels, the column names are
unchanged, and the physical plan still uses `TopK(fetch=2)`.
### Displaying labels
| Consumer | Behavior
|
| ----------------------------------------------- |
--------------------------------------------------------------------------------------
|
| `datafusion-cli` table format | Show the label as the
header. |
| `datafusion-cli` CSV, TSV, JSON, NDJSON formats | Keep column names.
Programs parse these formats, and JSON object keys must be unique. |
| `DataFrame::show()` | Show the label as the
header. |
| Arrow IPC and Flight SQL clients | Receive the metadata
with the schema. A client can choose to display it. |
| Other clients | Display column names, as
they do today. |
A display tool rebuilds each batch with a renamed schema
(`RecordBatch::try_new` with the same arrays), which copies no data.
### What changes when you turn on the option
The configuration option and rollout stay as proposed: opt-in, default
`false`, turned on first in `datafusion-cli`. Because no names change, scripts
that parse `datafusion-cli` CSV or JSON output keep working. The compatibility
table becomes:
| Affected area
| Effect
| Impact |
|
-------------------------------------------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------------------------------------------------------------------------------------
| ------ |
| Code that checks a result batch against a schema that it built itself,
such as `MemTable::try_new(schema, batches)` | Arrow's `Schema::contains`
requires matching field metadata, so the check fails with `Mismatch between
schema and batches`. Using `df.schema()` avoids this. | Medium |
| `DataFrame::union` of a labeled result with another input
| Field metadata can differ between
the inputs.
| Medium |
| Tests that print table headers through `DataFrame::show()` or
`datafusion-cli` | Headers show labels.
| Medium |
| `EXPLAIN` output
| The top projection shows `<name> AS
<name>` for labeled columns.
| Low |
| Files written from a labeled result, such as `df.write_parquet()`
| The embedded Arrow schema stores the
label metadata. Column names don't change.
| Low |
| Substrait and protobuf round-trip tests
| Field metadata can be lost in the
round trip.
| Low |
### Additional required code changes
- Add the metadata key constants and a helper that returns the display name
of a field: the label when `datafusion.label_of` equals the field name, and the
field name otherwise.
- Use the helper in the `datafusion-cli` table format
(`datafusion-cli/src/print_format.rs`) and in `DataFrame::show()`.
- Add tests for duplicate labels and for `with_column_renamed` on a labeled
result.
- In `output-field-name-semantic.md`, describe the label metadata instead of
replacing naming rules. Field names keep the rules in the specification.
### Renaming becomes an alternative
Renaming the output columns to their labels moves to "alternatives
considered". A rename helper, such as `DataFrame::with_display_names()`, can be
built on top of the metadata later for clients that want readable names, and it
can handle collisions in one place.
### Updated open questions
1. **Option name and namespace.** Is `datafusion.sql_parser.column_labels` a
good name, or does the option belong in another namespace, such as
`datafusion.format`?
2. **Metadata keys.** Are `datafusion.label` and `datafusion.label_of` good
names? Neither the Arrow format nor Flight SQL defines a display-label key. The
Arrow format reserves only `ARROW:extension:name` and
`ARROW:extension:metadata`, and recommends namespaced keys for application
metadata. `ARROW:FLIGHT:SQL:REMARKS` is defined as a column comment, but the
Arrow Flight SQL JDBC driver maps it to `ResultSetMetaData.getColumnLabel()`.
Should a Flight SQL server built on DataFusion also copy the label into
`REMARKS` so that JDBC tools show it?
3. **Library default.** Labels change no names, so should the option default
to `true` after the `datafusion-cli` rollout, given the schema-metadata effects
above?
4. **Machine-readable CLI formats.** Should CSV and TSV headers in
`datafusion-cli` show labels, given that duplicate labels are possible?
5. **Session-dependent labels.** Unchanged from the original question 5.
6. **Custom window display.** Unchanged from the original question 6.
If this direction sounds right, I'll update the issue description to match.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]