Skip to content

Add the plain codec format to the columnar - #3179

Merged
fulmicoton merged 4 commits into
mainfrom
paul.masurel/plain-codec-1-format
Oct 26, 2026
Merged

fulmicoton merged 4 commits into
mainfrom
paul.masurel/plain-codec-1-format

Conversation

@fulmicoton-dd

@fulmicoton-dd fulmicoton-dd commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Part 1/4 of the plain codec for Str / Bytes fast fields.

Today, text fast fields are always dictionary encoded. The plain codec stores the values themselves, without term ordinals. It is meant for columns with a high cardinality. The codec is chosen by configuration (PR 3), never detected.

This PR adds the format and the storage. The columnar writer does not produce plain columns yet (PR 2).

  • StrColumnCodec { Dictionary, Plain }.
  • Column key code: column_type as before, 0x10 | column_type for plain columns (0x13 bytes, 0x14 str).
  • Columnar version V3, written only if the columnar contains a plain column. Columnars without plain columns are byte-identical to today. Older readers fail cleanly at open on V3 (without the bump, iter_columns would panic on the unknown key code).
  • Plain column storage: blocks of at most 100000 values and 1 MiB of raw bytes. Each block is self-contained: its own OnPair16 dictionary (onpair 0.2.1), codes bitpacked on ceil(log2(num_tokens)) bits (raw u16 at 16 bits), per-block code offsets. Small or incompressible blocks are stored raw.
  • Block index (value starts, byte starts) read at open. Blocks are read lazily, one at a time: reading a value reads one block. PlainValueReader keeps the last block.
  • DynamicColumn::PlainStr / PlainBytes. open_u64_lenient returns an error on plain columns (they have no u64 representation).
  • MSRV 1.88 → 1.91, required by onpair.
  • Format documented in columnar/README.md.

PlainColumnSerializer is only used by tests until PR 2: it has a temporary allow(dead_code).

Existing tests modified:

  • format_version::test_version_serialization: 3 valid versions instead of 2.
  • columnar/src/tests.rs: one new arm in two exhaustive DynamicColumn matches.

🤖 Generated with Claude Code

@fulmicoton-dd
fulmicoton-dd force-pushed the paul.masurel/plain-codec-1-format branch 9 times, most recently from 064287d to 25f945b Compare October 15, 2026 10:27
@fulmicoton
fulmicoton force-pushed the paul.masurel/plain-codec-1-format branch 2 times, most recently from ee4a315 to c1ad2da Compare October 21, 2026 12:39
@fulmicoton
fulmicoton requested a review from Mallets October 21, 2026 12:40
@fulmicoton
fulmicoton marked this pull request as ready for review October 21, 2026 12:40
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review 🔄 Running since 2026-10-26T10:09:06.237602Z b3ea895 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c1ad2dad3b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +119 to +122
if block_index.value_start(0) != 0 || block_index.byte_start(0) != 0 {
return Err(invalid_data("plain block index does not start at 0"));
}
Ok(block_index)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject non-monotonic block indexes

When a damaged column contains repeated or decreasing entries in VALUE_STARTS or BYTE_STARTS, open accepts it because only the initial zero entries are checked. Subsequent calls assume strict ordering: block_containing performs a partition-point search and byte_range constructs a slice range, so reading such a column can select the wrong block or panic rather than report invalid data. Validate every adjacent pair in both arrays before returning the index.

Useful? React with 👍 / 👎.

Comment thread columnar/src/column/plain/LAYOUT.md
Comment thread columnar/src/column/plain/mod.rs Outdated
Comment thread columnar/src/column/plain/mod.rs Outdated
Comment thread columnar/src/dynamic_column.rs
@@ -241,21 +271,53 @@ static_dynamic_conversions!(Column<DateTime>, DateTime);
static_dynamic_conversions!(StrColumn, Str);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This macro is where the None from my comment on open() comes from. Every variant other than Str turns into None, including PlainStr. Since From can't fail, the fact that the column is plain-encoded is lost here.

If str()/bytes() match on the variant directly, could these two invocations (Str/Bytes) go away? Then no new caller can hit the silent None path by accident.

use crate::column_index::{SerializableColumnIndex, serialize_column_index};

/// A block is closed once it holds this many values.
pub(crate) const PLAIN_BLOCK_MAX_NUM_VALUES: usize = 100_000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The PR description says blocks hold at most 65,536 values and 1 MiB. The code, LAYOUT.md and the module docs say 100,000 values and 10 MiB. Which one is intended? Reading one value loads its whole block, so this matters for random reads.

Comment thread columnar/src/column/plain/block_index.rs
/// Creates a ColumnSerializer.
///
/// Precondition: `codec` is `Dictionary` unless `column_type` is `Str` or `Bytes`.
pub fn start_serialize_column_with_codec<'a>(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing stops the same name and type from being written with both codecs. The two keys don't collide, so both columns end up on disk and lookups return whichever comes first. Can we reject the duplicate here, or add a comment that the writer must use one codec per field?

Comment thread columnar/src/dynamic_column.rs
Comment thread Cargo.toml
keywords = ["search", "information", "retrieval"]
edition = "2021"
rust-version = "1.88"
rust-version = "1.91"

@Mallets Mallets Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does the MSRV bump need a changelog entry? Earlier bumps don't seem to be in CHANGELOG.md so I don't think it's the case.

Adds StrColumnCodec, the column key code of plain columns, the V3
columnar version, and the plain column storage: OnPair16 blocks of at
most 65,536 values and 1 MiB, block index, and reader.

The columnar writer does not produce plain columns yet.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46c3fddb28

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +108 to +109
let block_index = PlainBlockIndex::open(block_index_bytes)?;
let column_index = open_column_index(column_index_bytes, format_version)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate the column index value count

When a damaged plain column has a valid block index and column index that describe different numbers of values, open accepts both independently. For example, an optional index can reference a value beyond block_index.num_values(), causing append_first_value to panic, while an empty index paired with nonempty blocks silently makes stored values unreachable. Compare the value count represented by non-full column indexes with the block index before returning the column.

Useful? React with 👍 / 👎.

@fulmicoton
fulmicoton force-pushed the paul.masurel/plain-codec-1-format branch from 46c3fdd to 41da571 Compare October 26, 2026 10:03
@fulmicoton
fulmicoton force-pushed the paul.masurel/plain-codec-1-format branch from 41da571 to b3ea895 Compare October 26, 2026 10:08
@fulmicoton
fulmicoton merged commit 6bced0b into main Oct 26, 2026
7 checks passed
@fulmicoton
fulmicoton deleted the paul.masurel/plain-codec-1-format branch October 26, 2026 10:09
@fulmicoton-dd fulmicoton-dd mentioned this pull request Oct 26, 2026
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.

3 participants