Skip to content

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

Open
aaaronme wants to merge 1 commit intoquestdb:mainfrom
aaaronme:feat/buffer-opt-methods
Open

feat(ingress): add _opt builder methods to Buffer for Option<T>#134
aaaronme wants to merge 1 commit intoquestdb:mainfrom
aaaronme:feat/buffer-opt-methods

Conversation

@aaaronme
Copy link

@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

    • Introduced optional parameter variants for buffer methods, providing a more elegant approach to handling NULL values while preserving the fluent API design during data ingestion operations. Allows developers to conditionally include columns without breaking method chains.
  • Documentation

    • Added comprehensive guide explaining NULL representation and demonstrating usage patterns for optional parameters, including practical examples for conditional column writing in ingestion workflows.

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

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

Adds eight optional wrapper methods to the Buffer struct for ergonomic handling of Option-wrapped values in the fluent API. Each method delegates to its non-optional counterpart when Some(value) is provided and returns Ok(self) as a no-op when None, preserving method chaining without breaking the builder pattern.

Changes

Cohort / File(s) Summary
Buffer API Extensions
questdb-rs/src/ingress/buffer.rs
Added 8 public methods (symbol_opt, column_bool_opt, column_i64_opt, column_f64_opt, column_str_opt, column_dec_opt, column_arr_opt, column_ts_opt) that wrap existing non-optional counterparts with Option handling, maintaining fluent builder semantics across all generic constraints.
Documentation
questdb-rs/src/ingress/mod.md
New "Handling Optional Data (NULLs)" section explains QuestDB's NULL representation via column omission, documents the _opt API variants, clarifies ownership semantics for Copy vs non-Copy types, and provides a practical example using Option-wrapped struct fields.
Tests
questdb-rs/src/tests/buffer_opt.rs
New comprehensive test module validating _opt methods across ProtocolVersion variants: byte-for-byte equivalence with standard methods when Some, correct NULL handling when all values are None, and protocol-specific encoding for arrays and decimals.
Test Module Registration
questdb-rs/src/tests/mod.rs
Added module declaration for buffer_opt test suite.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 With _opt in my burrow, no chains to break,
Some flows right through, None's just a no-op's sake,
Fluent and nimble, my builder stands tall,
Handling NULLs gracefully, I skip them all!

🚥 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 clearly and accurately describes the main change: adding optional builder methods to Buffer for Option.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #133: eight _opt methods added (symbol_opt, column_bool_opt, column_i64_opt, column_f64_opt, column_str_opt, column_dec_opt, column_arr_opt, column_ts_opt) that accept Option, delegate to standard methods when Some(v), and return Ok(self) when None.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the requested _opt variants: Buffer methods (8 new methods), documentation updates explaining NULL handling, and comprehensive test coverage for the new API.
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
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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

@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 on lines +1271 to +1273
/// **Tip:** If you have an `Option<Decimal>`, use `.as_ref()` to pass it
/// without consuming the original string.
///
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix wording in ownership tip for decimal options.

Line 1272 says “without consuming the original string,” but this method accepts generic decimal values, not only strings.

✏️ Suggested text change
-    /// without consuming the original string.
+    /// without consuming the original value.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// **Tip:** If you have an `Option<Decimal>`, use `.as_ref()` to pass it
/// without consuming the original string.
///
/// **Tip:** If you have an `Option<Decimal>`, use `.as_ref()` to pass it
/// without consuming the original value.
///
🤖 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 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.

Comment on lines +1281 to +1297
/// ```no_run
/// # use questdb::Result;
/// # use questdb::ingress::{Buffer, SenderBuilder};
/// use bigdecimal::BigDecimal;
/// use std::str::FromStr;
/// # fn main() -> Result<()> {
/// # let mut sender = SenderBuilder::from_conf("https::addr=localhost:9000;")?.build()?;
/// # let mut buffer = sender.new_buffer();
/// # buffer.table("x")?;
/// let val: Option<BigDecimal> = Some(BigDecimal::from_str("0.123456789012345678901234567890").unwrap());
/// buffer.column_dec_opt("col_name", val)?;
///
/// let no_val: Option<BigDecimal> = None;
/// buffer.column_dec_opt("skipped_col", no_val)?;
/// # Ok(())
/// # }
/// ```
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if file exists first
if [ ! -f "questdb-rs/src/ingress/buffer.rs" ]; then
  echo "File not found. Listing repository structure..."
  find . -name "buffer.rs" -type f 2>/dev/null | head -20
  exit 0
fi

# Verify bigdecimal doc blocks and feature guards in buffer docs
echo "=== Searching for feature gates and bigdecimal usage ==="
rg -n -C3 'cfg\(feature = "bigdecimal"\)|use bigdecimal::BigDecimal|column_dec_opt' questdb-rs/src/ingress/buffer.rs | head -100

# Inspect the new column_dec_opt example area (lines 1278-1302)
echo ""
echo "=== Lines 1278-1302 in buffer.rs ==="
nl -ba questdb-rs/src/ingress/buffer.rs | sed -n '1278,1302p'

Repository: questdb/c-questdb-client

Length of output: 1500


Gate the bigdecimal doctest behind its feature flag.

Line 1284 introduces use bigdecimal::BigDecimal; without #[cfg(feature = "bigdecimal")], unlike the existing column_dec doctest (lines 1223-1233). This will break doctest compilation when bigdecimal is not enabled.

Proposed doc fix
-    /// ```no_run
-    /// # use questdb::Result;
-    /// # use questdb::ingress::{Buffer, SenderBuilder};
-    /// use bigdecimal::BigDecimal;
-    /// use std::str::FromStr;
+    /// ```no_run
+    /// # #[cfg(feature = "bigdecimal")]
+    /// # {
+    /// # use questdb::Result;
+    /// # use questdb::ingress::{Buffer, SenderBuilder};
+    /// use bigdecimal::BigDecimal;
+    /// use std::str::FromStr;
     /// # fn main() -> Result<()> {
     /// # let mut sender = SenderBuilder::from_conf("https::addr=localhost:9000;")?.build()?;
     /// # let mut buffer = sender.new_buffer();
     /// # buffer.table("x")?;
     /// let val: Option<BigDecimal> = Some(BigDecimal::from_str("0.123456789012345678901234567890").unwrap());
     /// buffer.column_dec_opt("col_name", val)?;
     ///
     /// let no_val: Option<BigDecimal> = None;
     /// buffer.column_dec_opt("skipped_col", no_val)?;
     /// # Ok(())
     /// # }
+    /// # }
     /// ```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// ```no_run
/// # use questdb::Result;
/// # use questdb::ingress::{Buffer, SenderBuilder};
/// use bigdecimal::BigDecimal;
/// use std::str::FromStr;
/// # fn main() -> Result<()> {
/// # let mut sender = SenderBuilder::from_conf("https::addr=localhost:9000;")?.build()?;
/// # let mut buffer = sender.new_buffer();
/// # buffer.table("x")?;
/// let val: Option<BigDecimal> = Some(BigDecimal::from_str("0.123456789012345678901234567890").unwrap());
/// buffer.column_dec_opt("col_name", val)?;
///
/// let no_val: Option<BigDecimal> = None;
/// buffer.column_dec_opt("skipped_col", no_val)?;
/// # Ok(())
/// # }
/// ```
///
🤖 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 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.

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

1 participant