Skip to content

feat(ingress): add _opt builder methods to Buffer for Option<T>#134

Merged
bluestreak01 merged 4 commits intoquestdb:mainfrom
aaaronme:feat/buffer-opt-methods
Apr 24, 2026
Merged

feat(ingress): add _opt builder methods to Buffer for Option<T>#134
bluestreak01 merged 4 commits intoquestdb:mainfrom
aaaronme:feat/buffer-opt-methods

Conversation

@aaaronme
Copy link
Copy Markdown
Contributor

@aaaronme aaaronme commented Feb 28, 2026

Closes #133

This PR introduces native support for Option<T> in the Buffer builder API to drastically improve the developer experience when ingesting real-world data with nullable fields.

The Problem:

Previously, handling optional data forced users to break the ergonomic method chain:

buffer.table("trades")?.symbol("symbol", "BTC-USD")?;

if let Some(fee) = trade.maker_fee {
    buffer.column_f64("maker_fee", fee)?;
}
if let Some(order_id) = trade.order_id.as_deref() {
    buffer.column_str("order_id", order_id)?;
}

buffer.at_now()?;

The Solution:

This PR adds _opt variants for all column types (column_str_opt, column_f64_opt, column_arr_opt, etc.). If the value is Some, it behaves exactly like the standard method. If None, it is a no-op, skipping the column perfectly in line with ILP standards.

buffer
    .table("trades")?
    .symbol("symbol", "BTC-USD")?
    .column_f64_opt("maker_fee", trade.maker_fee)?
    .column_str_opt("order_id", trade.order_id.as_deref())?
    .at_now()?;

Changes made

Added _opt methods directly to impl Buffer using the exact same zero-cost generic constraints as their standard counterparts.

Added a new section to the module-level documentation (# Usage Considerations) explaining how to handle NULLs and how to correctly pass ownership of heap-allocated Option types (e.g., using .as_deref()).

Documented every new method using intra-doc links to avoid duplicating complex API constraints.

Testing

As requested in the issue, a rigorous integration test suite was added in tests/buffer_opt.rs:

test_buffer_opt_some_matches_standard: Uses ProtocolVersion::V3 to construct one buffer with the standard methods, and one buffer with the _opt methods wrapped in Some. Validates that the resulting binary byte arrays are 100% identical (covering Strings, Numbers, Decimals, and Arrays).

test_buffer_opt_none_skips_columns: Asserts that passing None results in valid ILP syntax where the columns are entirely omitted from the UTF-8 output.

Summary by CodeRabbit

  • New Features

    • Added optional-parameter buffer APIs so fields wrapped in Option can be conditionally omitted while preserving fluent chaining.
  • Documentation

    • Added a guide on representing NULLs and examples showing conditional column writing with optional parameters.
  • Tests

    • Added tests ensuring optional-parameter behavior matches non-optional behavior for present values and correctly omits columns for None.

Adds `_opt` variants for all column methods on the `Buffer` struct
(e.g., `column_str_opt`, `column_i64_opt`, `column_arr_opt`). This
allows users to ergonomically handle nullable database fields
without breaking the fluent builder chain with `if let` statements.
When passed `None`, the methods act as a no-op, which naturally maps
to QuestDB's handling of NULLs (omitted columns in ILP).

- Added `*_opt` methods for symbol, bool, i64, f64, str, dec, arr, and ts.
- Kept generic constraints identical to standard methods for zero overhead.
- Added comprehensive module-level and method-level rustdocs.
- Added full integration test suite (`tests/buffer_opt.rs`) verifying
  exact byte-output matches for `Some`, and correct ILP syntax for `None`.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 28, 2026

Warning

Rate limit exceeded

@aaaronme has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 56 minutes and 55 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 56 minutes and 55 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: acd21ea1-f20c-4b79-be9c-a540357d1646

📥 Commits

Reviewing files that changed from the base of the PR and between f2448c6 and d0ccd79.

📒 Files selected for processing (1)
  • questdb-rs/src/ingress/buffer.rs
📝 Walkthrough

Walkthrough

Adds eight *_opt convenience methods to Buffer to accept Option<T> values and no-op on None, plus documentation explaining NULL semantics and tests validating byte-for-byte parity and column omission across protocol versions.

Changes

Cohort / File(s) Summary
Buffer API Extensions
questdb-rs/src/ingress/buffer.rs
Added eight public optional-wrapper methods: symbol_opt, column_bool_opt, column_i64_opt, column_f64_opt, column_str_opt, column_dec_opt, column_arr_opt, column_ts_opt. Each delegates to the existing non-optional method on Some and returns Ok(self) on None, preserving fluent chaining and existing generic bounds.
Documentation
questdb-rs/src/ingress/mod.md
Inserted a "Handling Optional Data (NULLs)" section describing QuestDB NULL semantics and the new _opt Buffer APIs, ownership notes for Copy vs non-Copy types, and example usage. The section appears twice in the diff.
Tests
questdb-rs/src/tests/buffer_opt.rs
New tests added: verify _opt(Some(...)) matches non-optional methods byte-for-byte; _opt(None) omits columns across V1/V2/V3; protocol-specific binary match for array encoding.
Test Module Registration
questdb-rs/src/tests/mod.rs
Registered the new buffer_opt test module so the added tests are included in the test suite.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hop through code with a soft little trot,

Some gives a value, None means "not",
Chains stay intact, no branches to mend,
Columns that vanish are NULL in the end,
I twitch my whiskers and patch with a hop.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: adding optional builder methods to the Buffer API for handling Option values.
Linked Issues check ✅ Passed All coding requirements from issue #133 are met: eight _opt variants added for column/symbol methods, each accepting Option, delegating to standard methods on Some, and returning Ok(self) on None.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing issue #133 requirements: _opt methods, documentation updates, and comprehensive tests validating the new functionality.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
questdb-rs/src/tests/buffer_opt.rs (1)

153-156: Prefer byte equality for decimal parity assertions.

Line 153-156 converts output to UTF-8 before comparing. For protocol-encoded payloads, direct as_bytes() equality is a stricter and less encoding-coupled assertion.

♻️ Suggested test tweak
-    assert_eq!(
-        std::str::from_utf8(opt_buf.as_bytes())?,
-        std::str::from_utf8(std_buf.as_bytes())?
-    );
+    assert_eq!(opt_buf.as_bytes(), std_buf.as_bytes());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@questdb-rs/src/tests/buffer_opt.rs` around lines 153 - 156, Replace the UTF-8
string comparison with a direct byte comparison: instead of converting
opt_buf.as_bytes() and std_buf.as_bytes() to UTF-8 and asserting string
equality, assert equality of the raw byte slices returned by opt_buf.as_bytes()
and std_buf.as_bytes(); locate the assertion using the symbols opt_buf, std_buf
and as_bytes() in the test function in buffer_opt.rs and change the assertion to
compare the byte slices directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@questdb-rs/src/ingress/buffer.rs`:
- Around line 1271-1273: Update the doc comment in ingress::buffer.rs that
currently reads “If you have an `Option<Decimal>`, use `.as_ref()` to pass it
without consuming the original string” to remove the word “string” and instead
reference the generic decimal value; e.g. change the phrase to “without
consuming the original value” (or similar) so the tip about using `.as_ref()`
for `Option<Decimal>` applies to any Decimal type, not only strings.
- Around line 1281-1297: The doctest for column_dec_opt uses bigdecimal
unguarded and will fail when the "bigdecimal" feature is disabled; wrap the
doctest block that imports bigdecimal and uses BigDecimal with a #[cfg(feature =
"bigdecimal")] guard (matching the pattern used in the earlier column_dec
doctest) so the use bigdecimal::BigDecimal and the test code only compile when
the feature is enabled; update the doctest surrounding the column_dec_opt
example accordingly.

---

Nitpick comments:
In `@questdb-rs/src/tests/buffer_opt.rs`:
- Around line 153-156: Replace the UTF-8 string comparison with a direct byte
comparison: instead of converting opt_buf.as_bytes() and std_buf.as_bytes() to
UTF-8 and asserting string equality, assert equality of the raw byte slices
returned by opt_buf.as_bytes() and std_buf.as_bytes(); locate the assertion
using the symbols opt_buf, std_buf and as_bytes() in the test function in
buffer_opt.rs and change the assertion to compare the byte slices directly.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 34905ab and d6cdf62.

📒 Files selected for processing (4)
  • questdb-rs/src/ingress/buffer.rs
  • questdb-rs/src/ingress/mod.md
  • questdb-rs/src/tests/buffer_opt.rs
  • questdb-rs/src/tests/mod.rs

Comment thread questdb-rs/src/ingress/buffer.rs
Comment thread questdb-rs/src/ingress/buffer.rs
   - Gate bigdecimal doctest behind `#[cfg(feature = "bigdecimal")]` and fix
     its `Option<BigDecimal>` usage to go through `.as_ref()` (TryInto is
     implemented for `&BigDecimal`, not `BigDecimal`).
   - Fix "original string" -> "original value" wording on column_dec_opt.
   - Demonstrate `.as_ref()` on `Option<Vec<...>>` in column_arr_opt doctest.
   - Expand None-skips test across V1/V2/V3 and drop single-value rstest
     wrappers on the other cases.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
questdb-rs/src/ingress/buffer.rs (2)

873-909: Behavior note: column_bool_opt(..., None) omits the column rather than writing false.

Worth calling out in the doc since, per QuestDB semantics, booleans are non-nullable and missing cells are conventionally filled with false on the server side. Skipping the column via None is consistent with the other _opt wrappers and with sparse-row handling elsewhere, but end users may expect "None ⇒ false" instead of "None ⇒ column omitted." A one-line clarification in the doc comment would prevent surprise.

Based on learnings: "In QuestDB, the boolean column type is non-nullable… when a boolean column is sparse (missing in some rows), the gaps are intentionally encoded as false rather than null."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@questdb-rs/src/ingress/buffer.rs` around lines 873 - 909, Update the doc
comment for the method column_bool_opt to explicitly state that passing None
will omit the column (no write) rather than write false, and mention QuestDB
semantics that boolean columns are non-nullable and server-side sparse/missing
boolean cells are interpreted/filled as false; reference the existing
convenience wrapper behavior around Self::column_bool so readers know how None
maps to "skip column" vs. an explicit call to column_bool(name, false).

1429-1479: Minor ergonomics asymmetry in column_arr_opt signature.

Unlike the other wrappers (which take Option<S> by value), column_arr_opt takes Option<&T>, so callers with an owned Option<Vec<...>> must remember to call .as_ref() (as the doctest shows). This mirrors column_arr's &T signature and is fine, but consider whether accepting Option<T> where T: NdArrayView<D> (and internally borrowing) would be more consistent with symbol_opt/column_str_opt. Not a blocker — the current doc tip is sufficient.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@questdb-rs/src/ingress/buffer.rs` around lines 1429 - 1479, Change
column_arr_opt to accept Option<T> by value (pub fn column_arr_opt<'a, N, T,
D>(&mut self, name: N, value: Option<T>) ...) where T: NdArrayView<D> so callers
can pass owned Option<Vec<...>> without .as_ref(); inside the Some arm borrow
the value (e.g., pass &v) into the existing self.column_arr(name, &v) and keep
the same trait bounds (N: TryInto<ColumnName<'a>>, D: ArrayElement +
ArrayElementSealed, Error: From<N::Error>) and return type crate::Result<&mut
Self> to preserve behavior and compatibility with column_arr and related
helpers.
questdb-rs/src/tests/buffer_opt.rs (1)

124-161: Optional: consolidate overlapping tests.

test_buffer_opt_array_some_binary_match (V2 array) and test_buffer_opt_decimal_some_matches_standard (V3 decimal) are already covered byte-for-byte by test_buffer_opt_some_matches_standard at V3 for arrays/decimals. The V2-array case is the only incremental coverage. Consider folding the V2 array check into the primary test (or a parametrized version across protocol versions) to reduce duplication; the decimal-specific test looks fully redundant.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@questdb-rs/src/tests/buffer_opt.rs` around lines 124 - 161, Consolidate
overlapping tests by removing the redundant V3 decimal test and folding the V2
array case into the main test_buffer_opt_some_matches_standard (or make that
test parameterized by ProtocolVersion); specifically, keep a single canonical
test that exercises Buffer::new, column_arr_opt/column_arr for arrays and
column_dec_opt/column_dec for decimals across the relevant ProtocolVersion(s)
(ensure V2 is included for arrays and V3 for decimals), and delete or merge the
tests test_buffer_opt_array_some_binary_match and
test_buffer_opt_decimal_some_matches_standard accordingly so byte-for-byte
comparisons still call as_bytes() on the same versioned Buffer instances.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@questdb-rs/src/ingress/buffer.rs`:
- Around line 873-909: Update the doc comment for the method column_bool_opt to
explicitly state that passing None will omit the column (no write) rather than
write false, and mention QuestDB semantics that boolean columns are non-nullable
and server-side sparse/missing boolean cells are interpreted/filled as false;
reference the existing convenience wrapper behavior around Self::column_bool so
readers know how None maps to "skip column" vs. an explicit call to
column_bool(name, false).
- Around line 1429-1479: Change column_arr_opt to accept Option<T> by value (pub
fn column_arr_opt<'a, N, T, D>(&mut self, name: N, value: Option<T>) ...) where
T: NdArrayView<D> so callers can pass owned Option<Vec<...>> without .as_ref();
inside the Some arm borrow the value (e.g., pass &v) into the existing
self.column_arr(name, &v) and keep the same trait bounds (N:
TryInto<ColumnName<'a>>, D: ArrayElement + ArrayElementSealed, Error:
From<N::Error>) and return type crate::Result<&mut Self> to preserve behavior
and compatibility with column_arr and related helpers.

In `@questdb-rs/src/tests/buffer_opt.rs`:
- Around line 124-161: Consolidate overlapping tests by removing the redundant
V3 decimal test and folding the V2 array case into the main
test_buffer_opt_some_matches_standard (or make that test parameterized by
ProtocolVersion); specifically, keep a single canonical test that exercises
Buffer::new, column_arr_opt/column_arr for arrays and column_dec_opt/column_dec
for decimals across the relevant ProtocolVersion(s) (ensure V2 is included for
arrays and V3 for decimals), and delete or merge the tests
test_buffer_opt_array_some_binary_match and
test_buffer_opt_decimal_some_matches_standard accordingly so byte-for-byte
comparisons still call as_bytes() on the same versioned Buffer instances.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c39044e7-4733-4a36-be89-eb1a0e987e7c

📥 Commits

Reviewing files that changed from the base of the PR and between d6cdf62 and 02c3607.

📒 Files selected for processing (2)
  • questdb-rs/src/ingress/buffer.rs
  • questdb-rs/src/tests/buffer_opt.rs

…ecimal test

- column_bool_opt: spell out that None skips the column entirely rather than
  writing false — readers wanting an explicit false should call column_bool
  directly.
- Remove test_buffer_opt_decimal_some_matches_standard; the main V3 test
  already round-trips column_dec_opt("c_dec", Some(...)) against column_dec.
@aaaronme aaaronme force-pushed the feat/buffer-opt-methods branch from 6fe6fc8 to a0e83be Compare April 22, 2026 07:49
@aaaronme
Copy link
Copy Markdown
Contributor Author

@amunra Failed check bc of unrelated code

@amunra
Copy link
Copy Markdown
Contributor

amunra commented Apr 22, 2026

@jerrinot and/or @mtopolnik might be able to help.

- Replaced nested `if` block with a match arm guard as suggested by clippy::collapsible_match.
- Improves code readability and adheres to idiomatic Rust patterns.
@aaaronme aaaronme force-pushed the feat/buffer-opt-methods branch from f2448c6 to d0ccd79 Compare April 22, 2026 22:27
@aaaronme
Copy link
Copy Markdown
Contributor Author

@amunra I added a small commit here to fix the issue, just clippy & fmt got updated after the new rust version. if it is all ok i think we are ready for a review. Version in question: cargo 1.95.0

@jerrinot
Copy link
Copy Markdown
Contributor

hello @aaaronme, thanks for this PR, this looks like a useful contribution. Increased API surface is a good price to pay to enable idiomatic Rust code.

@jerrinot
Copy link
Copy Markdown
Contributor

@aaaronme there is one thing you could do: a test to verify that every column_foo() has a column_foo_opt() twin.
I can think of a simple pattern matching on a source code, but perhaps you can come up with a better way. up to you.

@bluestreak01 bluestreak01 merged commit d63929b into questdb:main Apr 24, 2026
11 checks passed
@aaaronme
Copy link
Copy Markdown
Contributor Author

@jerrinot Thanks for the approval 🙌 I hope to be able to contribute more in the future :)

I'm thinking we pull in the buffer.rs source as a string (include_str!) and walk it: for each pub fn, look at its header up to the body's { and check whether it carries the N: TryInto<ColumnName<'a>> bound. That bound is the real "this is a column-writing method" signal. It catches symbol, every column_*, and any future addition like uuid that follows the same pattern, regardless of naming. Then for each one we just assert a sibling with _opt exists in the same scan. No regex, no new deps, ~25 lines.

If you want I can add it.
Should I commit it to this branch or open a new PR?

@jerrinot
Copy link
Copy Markdown
Contributor

@aaaronme no problem at all, thanks for your contribution!
Given this PR is already merged then a new PR is the cleanest. Keep the test simple please, I think even just a regexp would do.

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.

Ergonomic optional columns: Add _opt methods to Buffer

4 participants