Skip to content

Spec: Add collations for string types - #16972

Draft
laskoviymishka wants to merge 1 commit into
apache:mainfrom
laskoviymishka:spec-v3-collations
Draft

Spec: Add collations for string types#16972
laskoviymishka wants to merge 1 commit into
apache:mainfrom
laskoviymishka:spec-v3-collations

Conversation

@laskoviymishka

Copy link
Copy Markdown
Contributor

This adds collation support for string types to the v3 spec, following the discussion in the dev-list thread. A string field can carry a collation (e.g. icu.en_US-ci) that defines case-insensitive, accent-insensitive, or locale-aware comparison and ordering, and a new data_file.collation_bounds field carries collation-aware min/max so collated columns can still be pruned

The spec changes:

  • A collation attribute on string fields (provider-qualified, e.g. icu.en_US-ci; utf8 means byte order), stored unversioned in the schema so any compatible engine can read.
  • A new data_file.collation_bounds field (id 147): per column, a list of collation-aware lower/upper bounds stored as original values and tagged with the collation and the implementation version they were selected under.
  • Reader/writer rules: byte-order lower_bounds/upper_bounds are still written for collated columns so collation-unaware engines stay correct, but must not be used to prune predicates on a collated column; a collation_bounds entry may be used only on an exact collation + version match.

Two decisions I'd flag for discussion.

  • First, bounds store original values rather than ICU sort keys: sort keys aren't stable across UCA/CLDR/ICU versions, so per-file versioning plus an exact-match read gate degrades gracefully where a pinned global version would break.
  • Second, I put collation_bounds on data_file as a standalone v3 field, but it could instead live in the v4 content_stats typed-stats struct - worth deciding which.

Backed by a working reference implementation in iceberg-go: apache/iceberg-go#1318.

Rationale and the differences from the original proposal are written up separately (proposal doc).

See also the dev-list thread. Feedback very welcome.

@szehon-ho szehon-ho left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for working on this! A few comments here and inline:

  1. Nested string fields need an explicit story. The spec says a string field may carry a collation, and the JSON change adds collation to StructField. Does that intentionally exclude list<string>, map<string, ...>, and map<..., string>? If nested strings are in scope, the spec should define where collation is stored for list elements, map keys, and map values. If they are out of scope, that limitation should be explicit.

  2. Collation evolution can be better defined in terms of schema evolution. What happens when a column changes from one collation to another, or from byte-order to an ICU collation? Is this a compatible schema evolution, a type change, or disallowed? Existing files may contain bounds for older collations/versions, so readers should only use collation_bounds entries matching the collation resolved for the read schema and otherwise treat the file as possibly matching.

  3. Please clarify the scope beyond file pruning. If collation affects query semantics, then equality deletes, sort orders, partition transforms, and bucket/hash semantics may also need rules. If this change is only defining schema annotation and file-level pruning metadata, it would help to say that explicitly so engines do not infer broader SQL/equality semantics from the table-format metadata alone.

Comment thread format/spec.md Outdated
Comment thread format/spec.md Outdated
Comment thread format/spec.md
Comment thread format/spec.md Outdated
Comment thread format/spec.md Outdated
Comment thread format/spec.md Outdated
Comment thread format/spec.md Outdated
Comment thread format/spec.md Outdated
Comment thread format/spec.md Outdated
Add an optional `collation` annotation on string fields (top-level and
nested list/map positions) that changes comparison and ordering without
changing storage. Collation-aware file bounds are stored as ordinary
content_stats under schema-declared collation metric fields
(`collation-metrics`: id + collation + version), so a file can carry
bounds for multiple collations/versions and readers resolve them by
field id at planning time.

Scope is limited to the annotation and file-level pruning. Byte-order
bounds, partition/bucket transforms, equality-delete matching, and
identifier-field equality stay binary and collation-unaware; sort orders
are not defined here. Collation change is a metadata-only schema change.

The version records the provider's collation version (for `icu`, the ICU
collator version) and is compared as an opaque string; a mismatch falls
back to a scan, never a wrong result. Collation bounds must be tight.

Update the REST OpenAPI spec (yaml + generated models) with the
`collation` attribute and the CollationMetric type.
Comment thread format/spec.md
| `collation` | `string` | Collation the bounds are produced for, e.g. `icu.en_US-ci` |
| `version` | `string` | Provider's collation version the bounds were selected under (for `icu`, the ICU collator version); compared as an opaque string on read. See [Collations](#collations) |

The bounds themselves are an ordinary [field stats struct](#field-statistics) in `content_stats` under the metric field's id: its `lower_bound` and `upper_bound` are `string` values holding the file's minimum and maximum **under the collation order** — the original values, not collation sort keys (sort keys are not stable across implementation versions) — and `tight_bounds` indicates whether they are exact. Collation bounds must be tight: because there is no defined successor under an arbitrary collation order, a writer that cannot store the exact minimum and maximum must omit the bound rather than approximate it. A field may declare several collation metric entries — typically one per `(collation, version)` — so a single file can serve readers pinned to different versions during an upgrade.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Collation bounds must be tight: because there is no defined successor under an arbitrary collation order, a writer that cannot store the exact minimum and maximum must omit the bound rather than approximate it.

How would we define tight? Does it mean we do not allow truncation? I'm worried that omitting bounds could negatively impact performance

Comment thread format/spec.md
| `collation` | `string` | Collation the bounds are produced for, e.g. `icu.en_US-ci` |
| `version` | `string` | Provider's collation version the bounds were selected under (for `icu`, the ICU collator version); compared as an opaque string on read. See [Collations](#collations) |

The bounds themselves are an ordinary [field stats struct](#field-statistics) in `content_stats` under the metric field's id: its `lower_bound` and `upper_bound` are `string` values holding the file's minimum and maximum **under the collation order** — the original values, not collation sort keys (sort keys are not stable across implementation versions) — and `tight_bounds` indicates whether they are exact. Collation bounds must be tight: because there is no defined successor under an arbitrary collation order, a writer that cannot store the exact minimum and maximum must omit the bound rather than approximate it. A field may declare several collation metric entries — typically one per `(collation, version)` — so a single file can serve readers pinned to different versions during an upgrade.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

— the original values, not collation sort keys (sort keys are not stable across implementation versions)

that is a good point 👍 unfortunately, original strings are not stable across versions, ICU does not guarantee stable ordering, either.

If I understand correctly, this means that original strings provide no pruning benefits compared to sort keys, in the general case.

The situation would be different if we had some additional metrics on the data, though. Most of the ordering changes are limited to a small range of codepoints. If we had metadata proving that a file contains none of those (e.g., because it only contains ASCII), we could probably even prune across versions, which I'd consider a point in favor of sort keys

Comment thread format/spec.md

**Writers** are not required to write byte-order bounds for collated columns — as with any column, statistics are optional — but writing them is encouraged so collation-unaware readers can still prune. A writer that supports a declared `(collation, version)` should additionally write the `content_stats` struct for that metric field with the exact collation-order minimum and maximum. A writer that does not support the collation or version must not write bounds for that metric field.

**Readers** may prune a file using a collation metric field's bounds only when the metric's `collation` and `version` match the collation the reader resolves for the column; otherwise those bounds must be ignored. A reader must not use byte-order bounds to prune comparison, equality, or prefix predicates on a collated column. Collation bounds support comparison and equality pruning only: prefix predicates such as `STARTS_WITH` or `LIKE 'x%'` cannot be pruned by either byte-order or collation bounds, because prefix relationships are not preserved under an arbitrary collation order. When no usable collation bounds are available, the file must be scanned. A collation-unaware reader ignores the `collation` attribute and any collation metric fields, reading the column as a UTF-8 byte-order string.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

SQL standard supports pad characteristic (NO PAD | PAD SPACE). How do engines know whether icu.en_US-ci is pad-space or no-pad?

Comment thread format/spec.md

A collation may be attached to any field of `string` type, top-level or nested: as a `collation` attribute on a struct field, or as an `element-collation`, `key-collation`, or `value-collation` attribute on an enclosing `list` or `map` (see [Appendix C](#appendix-c-json-serialization)). It may be attached only to `string` types. A string field with no collation defaults to UTF-8 byte-order comparison, the behavior of all prior versions. Nesting does not change how collation bounds work: because every nested string position has its own field id, a collation on a nested string is annotated and given collation metrics exactly as a top-level string field (see [Collation Bounds](#collation-bounds)).

A collation is identified by a provider-qualified name of the form `<provider>.<name>`, for example `icu.en_US-ci`. The provider names the library that defines the collation (`icu` for collations defined by the [Unicode Collation Algorithm](https://unicode.org/reports/tr10/) over [CLDR](https://cldr.unicode.org/) locale data; other providers may define engine-specific collations such as case-folding variants). The name selects a locale and optional modifiers for case sensitivity (`ci`/`cs`), accent sensitivity (`ai`/`as`), trimming, and case folding.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In SQL standard, the collation name is a schema-qualified identifier. The suggested name format may not fit some engines.

Comment thread format/spec.md

SQL-level comparison, equality, ordering, grouping, and distinctness are the engine's responsibility. Engines read the `collation` annotation and apply their own semantics; they must not infer broader behavior from the table-format metadata alone.

A collation may be attached to any field of `string` type, top-level or nested: as a `collation` attribute on a struct field, or as an `element-collation`, `key-collation`, or `value-collation` attribute on an enclosing `list` or `map` (see [Appendix C](#appendix-c-json-serialization)). It may be attached only to `string` types. A string field with no collation defaults to UTF-8 byte-order comparison, the behavior of all prior versions. Nesting does not change how collation bounds work: because every nested string position has its own field id, a collation on a nested string is annotated and given collation metrics exactly as a top-level string field (see [Collation Bounds](#collation-bounds)).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Under a case-insensitive collation, should map['ABC'] and map['abc'] resolve to the same entry? And if so, what happens when a map contains both keys? Is that an invalid state, or is the result engine-defined?

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

Labels

Iceberg V4 Iceberg Table Format Version 4 OPENAPI Specification Issues that may introduce spec changes.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants