Skip to content

Commit 47ad6df

Browse files
committed
Address comments 3
1 parent 9fd91b3 commit 47ad6df

3 files changed

Lines changed: 129 additions & 14 deletions

File tree

bindings/cpp/examples/example.cpp

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,18 @@ int main() {
143143
std::cout << "Row acknowledged by server" << std::endl;
144144
}
145145

146+
// Append a row with all fields null (matches Rust log_table.rs all_supported_datatypes)
147+
{
148+
fluss::GenericRow row;
149+
size_t field_count = 8;
150+
for (size_t i = 0; i < field_count; ++i) {
151+
row.SetNull(i);
152+
}
153+
check("append_null_row", writer.Append(row));
154+
}
155+
check("flush_null", writer.Flush());
156+
std::cout << "Wrote row with all fields null" << std::endl;
157+
146158
// 6) Full scan — verify all column types including temporal
147159
fluss::LogScanner scanner;
148160
check("new_log_scanner", table.NewScan().CreateLogScanner(scanner));
@@ -158,7 +170,23 @@ int main() {
158170

159171
std::cout << "Scanned records: " << records.Size() << std::endl;
160172
bool scan_ok = true;
173+
bool found_null_row = false;
161174
for (const auto& rec : records) {
175+
// Check if this is the all-null row (matches Rust: is_null_at for every column)
176+
if (rec.row.IsNull(0)) {
177+
found_null_row = true;
178+
for (size_t i = 0; i < rec.row.FieldCount(); ++i) {
179+
if (!rec.row.IsNull(i)) {
180+
std::cerr << "ERROR: column " << i << " should be null" << std::endl;
181+
scan_ok = false;
182+
}
183+
}
184+
std::cout << " [null row] all " << rec.row.FieldCount() << " fields are null"
185+
<< std::endl;
186+
continue;
187+
}
188+
189+
// Non-null rows: verify types
162190
if (rec.row.GetType(4) != fluss::TypeId::Date) {
163191
std::cerr << "ERROR: field 4 expected Date, got "
164192
<< static_cast<int>(rec.row.GetType(4)) << std::endl;
@@ -194,6 +222,11 @@ int main() {
194222
<< ts_ltz.nano_of_millisecond << "ns" << std::endl;
195223
}
196224

225+
if (!found_null_row) {
226+
std::cerr << "ERROR: did not find the all-null row" << std::endl;
227+
scan_ok = false;
228+
}
229+
197230
if (!scan_ok) {
198231
std::cerr << "Full scan type verification FAILED!" << std::endl;
199232
std::exit(1);
@@ -220,6 +253,11 @@ int main() {
220253
scan_ok = false;
221254
continue;
222255
}
256+
// Skip the all-null row
257+
if (rec.row.IsNull(0)) {
258+
std::cout << " [null row] skipped" << std::endl;
259+
continue;
260+
}
223261
if (rec.row.GetType(0) != fluss::TypeId::Int) {
224262
std::cerr << "ERROR: projected field 0 expected Int, got "
225263
<< static_cast<int>(rec.row.GetType(0)) << std::endl;
@@ -256,6 +294,11 @@ int main() {
256294
scan_ok = false;
257295
continue;
258296
}
297+
// Skip the all-null row
298+
if (rec.row.IsNull(0)) {
299+
std::cout << " [null row] skipped" << std::endl;
300+
continue;
301+
}
259302
if (rec.row.GetType(0) != fluss::TypeId::Int) {
260303
std::cerr << "ERROR: name-projected field 0 expected Int, got "
261304
<< static_cast<int>(rec.row.GetType(0)) << std::endl;

bindings/cpp/examples/kv_example.cpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,57 @@ int main() {
176176
}
177177
}
178178

179+
// 4b) Null row round-trip (matches Rust kv_table.rs all_supported_datatypes)
180+
// Upsert a row with all non-PK fields null, lookup, verify IsNull
181+
std::cout << "\n--- Null Row Round-Trip ---" << std::endl;
182+
{
183+
auto row = kv_table.NewRow();
184+
row.Set("user_id", 100);
185+
row.SetNull(1); // name
186+
row.SetNull(2); // email
187+
row.SetNull(3); // score
188+
row.SetNull(4); // balance
189+
row.SetNull(5); // birth_date
190+
row.SetNull(6); // login_time
191+
row.SetNull(7); // created_at
192+
row.SetNull(8); // last_seen
193+
fluss::WriteResult wr;
194+
check("upsert_null_row", upsert_writer.Upsert(row, wr));
195+
check("upsert_null_row_wait", wr.Wait());
196+
}
197+
{
198+
auto pk_row = kv_table.NewRow();
199+
pk_row.Set("user_id", 100);
200+
201+
fluss::LookupResult result;
202+
check("lookup_null_row", lookuper.Lookup(pk_row, result));
203+
if (!result.Found()) {
204+
std::cerr << "ERROR: Expected to find user_id=100 (null row)" << std::endl;
205+
std::exit(1);
206+
}
207+
208+
// Verify PK is not null
209+
if (result.IsNull(0)) {
210+
std::cerr << "ERROR: PK (user_id) should not be null" << std::endl;
211+
std::exit(1);
212+
}
213+
214+
// Verify all nullable columns are null (matches Rust is_null_at assertions)
215+
bool null_ok = true;
216+
for (size_t i = 1; i < result.FieldCount(); ++i) {
217+
if (!result.IsNull(i)) {
218+
std::cerr << "ERROR: column " << i << " should be null" << std::endl;
219+
null_ok = false;
220+
}
221+
}
222+
if (null_ok) {
223+
std::cout << "Null row verified: all " << (result.FieldCount() - 1)
224+
<< " nullable fields are null" << std::endl;
225+
} else {
226+
std::exit(1);
227+
}
228+
}
229+
179230
// 5) Update via upsert (overwrite existing key)
180231
std::cout << "\n--- Update via Upsert ---" << std::endl;
181232
{

bindings/cpp/src/table.cpp

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ int Date::Day() const {
7979
return tm.tm_mday;
8080
}
8181

82+
static void check_generic_row_available(const ffi::GenericRowInner* inner) {
83+
if (!inner) {
84+
throw std::logic_error("GenericRow: not available (moved-from or null)");
85+
}
86+
}
87+
8288
// ============================================================================
8389
// GenericRow — write-only row backed by opaque Rust GenericRowInner
8490
// ============================================================================
@@ -100,6 +106,7 @@ void GenericRow::Destroy() noexcept {
100106
rust::Box<ffi::GenericRowInner>::from_raw(inner_);
101107
inner_ = nullptr;
102108
}
109+
column_map_.reset();
103110
}
104111

105112
GenericRow::GenericRow(GenericRow&& other) noexcept
@@ -120,54 +127,68 @@ GenericRow& GenericRow::operator=(GenericRow&& other) noexcept {
120127
bool GenericRow::Available() const { return inner_ != nullptr; }
121128

122129
void GenericRow::Reset() {
123-
if (inner_) inner_->gr_reset();
130+
check_generic_row_available(inner_);
131+
inner_->gr_reset();
124132
}
125133

126134
void GenericRow::SetNull(size_t idx) {
127-
if (inner_) inner_->gr_set_null(idx);
135+
check_generic_row_available(inner_);
136+
inner_->gr_set_null(idx);
128137
}
129138
void GenericRow::SetBool(size_t idx, bool v) {
130-
if (inner_) inner_->gr_set_bool(idx, v);
139+
check_generic_row_available(inner_);
140+
inner_->gr_set_bool(idx, v);
131141
}
132142
void GenericRow::SetInt32(size_t idx, int32_t v) {
133-
if (inner_) inner_->gr_set_i32(idx, v);
143+
check_generic_row_available(inner_);
144+
inner_->gr_set_i32(idx, v);
134145
}
135146
void GenericRow::SetInt64(size_t idx, int64_t v) {
136-
if (inner_) inner_->gr_set_i64(idx, v);
147+
check_generic_row_available(inner_);
148+
inner_->gr_set_i64(idx, v);
137149
}
138150
void GenericRow::SetFloat32(size_t idx, float v) {
139-
if (inner_) inner_->gr_set_f32(idx, v);
151+
check_generic_row_available(inner_);
152+
inner_->gr_set_f32(idx, v);
140153
}
141154
void GenericRow::SetFloat64(size_t idx, double v) {
142-
if (inner_) inner_->gr_set_f64(idx, v);
155+
check_generic_row_available(inner_);
156+
inner_->gr_set_f64(idx, v);
143157
}
144158

145159
void GenericRow::SetString(size_t idx, std::string v) {
146-
if (inner_) inner_->gr_set_str(idx, v);
160+
check_generic_row_available(inner_);
161+
inner_->gr_set_str(idx, v);
147162
}
148163

149164
void GenericRow::SetBytes(size_t idx, std::vector<uint8_t> v) {
150-
if (inner_) inner_->gr_set_bytes(idx, rust::Slice<const uint8_t>(v.data(), v.size()));
165+
check_generic_row_available(inner_);
166+
inner_->gr_set_bytes(idx, rust::Slice<const uint8_t>(v.data(), v.size()));
151167
}
152168

153169
void GenericRow::SetDate(size_t idx, fluss::Date d) {
154-
if (inner_) inner_->gr_set_date(idx, d.days_since_epoch);
170+
check_generic_row_available(inner_);
171+
inner_->gr_set_date(idx, d.days_since_epoch);
155172
}
156173

157174
void GenericRow::SetTime(size_t idx, fluss::Time t) {
158-
if (inner_) inner_->gr_set_time(idx, t.millis_since_midnight);
175+
check_generic_row_available(inner_);
176+
inner_->gr_set_time(idx, t.millis_since_midnight);
159177
}
160178

161179
void GenericRow::SetTimestampNtz(size_t idx, fluss::Timestamp ts) {
162-
if (inner_) inner_->gr_set_ts_ntz(idx, ts.epoch_millis, ts.nano_of_millisecond);
180+
check_generic_row_available(inner_);
181+
inner_->gr_set_ts_ntz(idx, ts.epoch_millis, ts.nano_of_millisecond);
163182
}
164183

165184
void GenericRow::SetTimestampLtz(size_t idx, fluss::Timestamp ts) {
166-
if (inner_) inner_->gr_set_ts_ltz(idx, ts.epoch_millis, ts.nano_of_millisecond);
185+
check_generic_row_available(inner_);
186+
inner_->gr_set_ts_ltz(idx, ts.epoch_millis, ts.nano_of_millisecond);
167187
}
168188

169189
void GenericRow::SetDecimal(size_t idx, const std::string& value) {
170-
if (inner_) inner_->gr_set_decimal_str(idx, value);
190+
check_generic_row_available(inner_);
191+
inner_->gr_set_decimal_str(idx, value);
171192
}
172193

173194
// ============================================================================

0 commit comments

Comments
 (0)