Skip to main content

hyperdb_api/
inserter.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! High-performance bulk data inserter using COPY protocol.
5//!
6//! This module provides the `Inserter` struct for efficient bulk data insertion
7//! into Hyper tables, along with the `IntoValue` trait for type-safe insertion.
8//!
9//! # Example
10//!
11//! ```no_run
12//! # use hyperdb_api::{Inserter, Connection, CreateMode, Result};
13//! # fn example(conn: &Connection, table_def: &hyperdb_api::TableDefinition) -> Result<()> {
14//! let mut inserter = Inserter::new(&conn, &table_def)?;
15//! for i in 0..10000i32 {
16//!     inserter.add_row(&[&i, &format!("item {}", i), &(i as f64 * 1.5)])?;
17//! }
18//! inserter.execute()?;
19//! # Ok(())
20//! # }
21//! ```
22
23use std::time::Instant;
24
25use hyperdb_api_core::client::client::CopyInWriter;
26use hyperdb_api_core::protocol::copy;
27use hyperdb_api_core::types::bytes::BytesMut;
28use hyperdb_api_core::types::{
29    Date, Geography, Interval, Numeric, OffsetTimestamp, Time, Timestamp,
30};
31use tracing::{debug, info};
32
33use crate::catalog::Catalog;
34use crate::connection::Connection;
35use crate::error::{Error, Result};
36use crate::table_definition::TableDefinition;
37
38/// Initial buffer size (4 MB) to reduce early reallocations.
39///
40/// The COPY protocol sends data in chunks, and each chunk requires a
41/// contiguous buffer. Starting at 4 MB avoids repeated reallocations
42/// during the first chunk for typical workloads while keeping initial
43/// memory allocation reasonable.
44const INITIAL_BUFFER_SIZE: usize = 4 * 1024 * 1024;
45
46/// Maximum buffer size per chunk before flushing to the server (16 MB).
47///
48/// This balances two competing concerns:
49/// - **Throughput**: Larger chunks amortize per-chunk overhead (COPY header,
50///   network round-trip). Below ~1 MB, per-chunk overhead becomes significant.
51/// - **Memory**: The buffer must be fully materialized before sending. 16 MB
52///   keeps resident memory bounded even when rows are wide.
53///
54/// The 16 MB value was chosen empirically — it lands on the flat part of
55/// the throughput curve where further increases yield diminishing returns.
56const CHUNK_SIZE_LIMIT: usize = 16 * 1024 * 1024;
57
58/// Maximum rows per chunk before flushing to the server.
59///
60/// This is a secondary flush trigger alongside [`CHUNK_SIZE_LIMIT`]. For
61/// narrow rows (few bytes each), the byte limit alone would accumulate
62/// millions of rows before flushing, which delays server-side processing.
63/// 64K rows ensures timely flushes regardless of row width and aligns with
64/// the 64K chunk size used for query result streaming
65/// ([`DEFAULT_BINARY_CHUNK_SIZE`](crate::result::DEFAULT_BINARY_CHUNK_SIZE)).
66const CHUNK_ROW_LIMIT: usize = 64_000;
67
68/// A high-performance bulk data inserter.
69///
70/// The `Inserter` efficiently inserts large amounts of data into a Hyper table
71/// using the COPY protocol with `HyperBinary` format for optimal performance.
72///
73/// # Example
74///
75/// ```no_run
76/// use hyperdb_api::{Connection, CreateMode, Catalog, TableDefinition, Inserter, SqlType, Result};
77///
78/// fn main() -> Result<()> {
79///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::CreateIfNotExists)?;
80///
81///     let table_def = TableDefinition::new("users")
82///         .add_required_column("id", SqlType::int())
83///         .add_nullable_column("name", SqlType::text());
84///
85///     Catalog::new(&conn).create_table(&table_def)?;
86///
87///     let mut inserter = Inserter::new(&conn, &table_def)?;
88///
89///     for i in 0..1000i32 {
90///         inserter.add_row(&[&i, &format!("User {}", i)])?;
91///     }
92///
93///     let rows = inserter.execute()?;
94///     println!("Inserted {} rows", rows);
95///     Ok(())
96/// }
97/// ```
98#[derive(Debug)]
99pub struct Inserter<'conn> {
100    connection: &'conn Connection,
101    table_def: TableDefinition,
102    /// The current chunk being populated (delegates encoding).
103    chunk: InsertChunk,
104    /// Total rows inserted across all chunks.
105    row_count: u64,
106    /// Number of chunks sent.
107    chunk_count: usize,
108    /// Active COPY writer (lazily initialized on first write).
109    writer: Option<CopyInWriter<'conn>>,
110    /// Start time for timing the insert operation.
111    start_time: Instant,
112}
113
114impl<'conn> Inserter<'conn> {
115    /// Creates a new inserter for the given table.
116    ///
117    /// The underlying COPY session is started lazily on the first flush or
118    /// execute, so construction is lightweight. However, the connection's
119    /// transport is validated eagerly — using a gRPC connection will return
120    /// an error immediately.
121    ///
122    /// # Errors
123    ///
124    /// - Returns [`Error::InvalidTableDefinition`] if `table_def` has zero
125    ///   columns.
126    /// - Returns [`Error::FeatureNotSupported`] if `connection` is using gRPC transport
127    ///   (COPY is TCP-only).
128    pub fn new(connection: &'conn Connection, table_def: &TableDefinition) -> Result<Self> {
129        if table_def.column_count() == 0 {
130            return Err(Error::invalid_table_definition(
131                "Table definition must have at least one column",
132            ));
133        }
134
135        // Fail fast: verify the connection supports COPY (TCP only)
136        if connection.tcp_client().is_none() {
137            return Err(Error::feature_not_supported(
138                "Inserter requires a TCP connection. \
139                 gRPC connections do not support COPY operations.",
140            ));
141        }
142
143        Ok(Inserter {
144            connection,
145            table_def: table_def.clone(),
146            chunk: InsertChunk::from_table_definition(table_def),
147            row_count: 0,
148            chunk_count: 0,
149            writer: None,
150            start_time: Instant::now(),
151        })
152    }
153
154    /// Creates an inserter by querying the table schema from the database.
155    ///
156    /// This method queries the database to get the table definition automatically,
157    /// which is useful when you want to insert into an existing table without
158    /// manually specifying the schema.
159    ///
160    /// # Arguments
161    ///
162    /// * `connection` - The database connection.
163    /// * `table_name` - The table name (can be a simple name, or "schema.table", etc.)
164    ///
165    /// # Errors
166    ///
167    /// Returns an error if the table doesn't exist or if the schema cannot be retrieved.
168    ///
169    /// # Example
170    ///
171    /// ```no_run
172    /// use hyperdb_api::{Connection, CreateMode, Inserter, Result};
173    ///
174    /// fn main() -> Result<()> {
175    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::CreateIfNotExists)?;
176    ///
177    ///     // Create a table first
178    ///     conn.execute_command("CREATE TABLE IF NOT EXISTS products (id INT NOT NULL, name TEXT, price DOUBLE PRECISION)")?;
179    ///
180    ///     // Create inserter by querying the schema directly from a string
181    ///     let mut inserter = Inserter::from_table(&conn, "public.products")?;
182    ///
183    ///     // Now we can insert data without knowing the exact schema
184    ///     inserter.add_row(&[&1i32, &"Widget", &19.99f64])?;
185    ///     inserter.add_row(&[&2i32, &"Gadget", &29.99f64])?;
186    ///
187    ///     let rows = inserter.execute()?;
188    ///     println!("Inserted {} rows", rows);
189    ///     Ok(())
190    /// }
191    /// ```
192    pub fn from_table<T>(connection: &'conn Connection, table_name: T) -> Result<Self>
193    where
194        T: TryInto<crate::TableName>,
195        crate::Error: From<T::Error>,
196    {
197        let catalog = Catalog::new(connection);
198        let table_def = catalog.get_table_definition(table_name)?;
199        Self::new(connection, &table_def)
200    }
201
202    /// Creates an inserter with column mappings that allow SQL expressions.
203    ///
204    /// This method uses a temporary table and INSERT...SELECT to support
205    /// column mappings with SQL expressions. Data is first inserted into
206    /// a temporary staging table, then transformed using the mappings.
207    ///
208    /// # Arguments
209    ///
210    /// * `connection` - The database connection.
211    /// * `inserter_def` - Defines the columns to be provided to the inserter (staging table).
212    /// * `target_table` - The qualified name of the target table to insert into.
213    ///   Use `TableDefinition::qualified_name()` for properly escaped names like `"schema"."table"`.
214    /// * `mappings` - Column mappings defining how values are transformed.
215    ///
216    /// # Example
217    ///
218    /// ```no_run
219    /// use hyperdb_api::{Connection, CreateMode, TableDefinition, ColumnMapping, Inserter, SqlType, Result};
220    ///
221    /// fn main() -> Result<()> {
222    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::CreateIfNotExists)?;
223    ///
224    ///     // Target table with computed columns
225    ///     conn.execute_command(r#"
226    ///         CREATE TABLE orders (
227    ///             id INT NOT NULL,
228    ///             product TEXT,
229    ///             quantity INT,
230    ///             price DOUBLE PRECISION,
231    ///             total DOUBLE PRECISION,
232    ///             created_at TIMESTAMP
233    ///         )
234    ///     "#)?;
235    ///
236    ///     // Inserter definition - what we provide
237    ///     let inserter_def = TableDefinition::new("_stage")
238    ///         .add_required_column("id", SqlType::int())
239    ///         .add_nullable_column("product", SqlType::text())
240    ///         .add_nullable_column("quantity", SqlType::int())
241    ///         .add_nullable_column("price", SqlType::double());
242    ///
243    ///     // Column mappings - how values are transformed
244    ///     let mappings = vec![
245    ///         ColumnMapping::new("id"),
246    ///         ColumnMapping::new("product"),
247    ///         ColumnMapping::new("quantity"),
248    ///         ColumnMapping::new("price"),
249    ///         ColumnMapping::with_expression("total", "quantity * price"),
250    ///         ColumnMapping::with_expression("created_at", "NOW()"),
251    ///     ];
252    ///
253    ///     // For simple table names in the public schema, use quoted name
254    ///     // For qualified names, use target_table_def.qualified_name()
255    ///     let mut inserter = Inserter::with_column_mappings(&conn, &inserter_def, "orders", &mappings)?;
256    ///
257    ///     inserter.add_row(&[&1i32, &"Widget", &5i32, &10.0f64])?;
258    ///     inserter.add_row(&[&2i32, &"Gadget", &3i32, &25.0f64])?;
259    ///
260    ///     let rows = inserter.execute()?;
261    ///     Ok(())
262    /// }
263    /// ```
264    ///
265    /// # Errors
266    ///
267    /// - Returns an error if `target_table` fails to convert into a
268    ///   [`TableName`](crate::TableName).
269    /// - Returns [`Error::Server`] if creating the temporary staging table
270    ///   fails on the server.
271    /// - Returns the errors from [`Inserter::new`] for the staging table
272    ///   (zero-column table definition, gRPC transport).
273    pub fn with_column_mappings<T>(
274        connection: &'conn Connection,
275        inserter_def: &TableDefinition,
276        target_table: T,
277        mappings: &[ColumnMapping],
278    ) -> Result<MappedInserter<'conn>>
279    where
280        T: TryInto<crate::TableName>,
281        crate::Error: From<T::Error>,
282    {
283        MappedInserter::new(connection, inserter_def, target_table, mappings)
284    }
285
286    /// Returns the table definition.
287    #[must_use]
288    pub fn table_definition(&self) -> &TableDefinition {
289        &self.table_def
290    }
291
292    /// Returns the number of columns.
293    #[must_use]
294    pub fn column_count(&self) -> usize {
295        self.table_def.column_count()
296    }
297
298    /// Returns the number of complete rows buffered.
299    #[must_use]
300    pub fn row_count(&self) -> u64 {
301        self.row_count
302    }
303
304    /// Adds a NULL value for the current column.
305    ///
306    /// # Errors
307    ///
308    /// Returns [`Error::InvalidTableDefinition`] if the current row already has all columns
309    /// supplied, or if the current column is marked `NOT NULL` in the table
310    /// definition.
311    #[inline]
312    pub fn add_null(&mut self) -> Result<()> {
313        self.chunk.add_null()
314    }
315
316    /// Adds a boolean value.
317    ///
318    /// # Errors
319    ///
320    /// Returns [`Error::InvalidTableDefinition`] with message `"Too many columns in row"` if
321    /// the current row already has all columns supplied.
322    #[inline]
323    pub fn add_bool(&mut self, value: bool) -> Result<()> {
324        self.chunk.add_bool(value)
325    }
326
327    /// Adds an i16 value (SMALLINT).
328    ///
329    /// # Errors
330    ///
331    /// See [`add_bool`](Self::add_bool).
332    #[inline]
333    pub fn add_i16(&mut self, value: i16) -> Result<()> {
334        self.chunk.add_i16(value)
335    }
336
337    /// Adds an i32 value (INT).
338    ///
339    /// # Errors
340    ///
341    /// See [`add_bool`](Self::add_bool).
342    #[inline]
343    pub fn add_i32(&mut self, value: i32) -> Result<()> {
344        self.chunk.add_i32(value)
345    }
346
347    /// Adds an i64 value (BIGINT).
348    ///
349    /// # Errors
350    ///
351    /// See [`add_bool`](Self::add_bool).
352    #[inline]
353    pub fn add_i64(&mut self, value: i64) -> Result<()> {
354        self.chunk.add_i64(value)
355    }
356
357    /// Adds an f32 value (REAL/FLOAT4).
358    ///
359    /// # Errors
360    ///
361    /// See [`add_bool`](Self::add_bool).
362    #[inline]
363    pub fn add_f32(&mut self, value: f32) -> Result<()> {
364        self.chunk.add_f32(value)
365    }
366
367    /// Adds an f64 value (DOUBLE PRECISION/FLOAT8).
368    ///
369    /// # Errors
370    ///
371    /// See [`add_bool`](Self::add_bool).
372    #[inline]
373    pub fn add_f64(&mut self, value: f64) -> Result<()> {
374        self.chunk.add_f64(value)
375    }
376
377    /// Adds a string value (TEXT/VARCHAR).
378    ///
379    /// # Errors
380    ///
381    /// See [`add_bool`](Self::add_bool).
382    #[inline]
383    pub fn add_str(&mut self, value: &str) -> Result<()> {
384        self.chunk.add_str(value)
385    }
386
387    /// Adds a bytes value (BYTEA).
388    ///
389    /// # Errors
390    ///
391    /// See [`add_bool`](Self::add_bool).
392    #[inline]
393    pub fn add_bytes(&mut self, value: &[u8]) -> Result<()> {
394        self.chunk.add_bytes(value)
395    }
396
397    /// Adds a 128-bit value (NUMERIC/INTERVAL).
398    ///
399    /// # Errors
400    ///
401    /// See [`add_bool`](Self::add_bool).
402    #[inline]
403    pub fn add_data128(&mut self, value: &[u8; 16]) -> Result<()> {
404        self.chunk.add_data128(value)
405    }
406
407    /// Adds an optional value. If None, adds NULL.
408    ///
409    /// # Errors
410    ///
411    /// Propagates whatever `add_fn` or [`add_null`](Self::add_null) would
412    /// return for the current row position.
413    pub fn add_optional<T, F>(&mut self, value: Option<T>, add_fn: F) -> Result<()>
414    where
415        F: FnOnce(&mut Self, T) -> Result<()>,
416    {
417        match value {
418            Some(v) => add_fn(self, v),
419            None => self.add_null(),
420        }
421    }
422
423    /// Ends the current row.
424    ///
425    /// Returns an error if the wrong number of columns were added.
426    /// Automatically flushes the buffer if chunk limits are reached.
427    ///
428    /// # Errors
429    ///
430    /// - Returns [`Error::InvalidTableDefinition`] if fewer (or more) columns were supplied
431    ///   than the table definition requires.
432    /// - Returns any error from [`flush`](Self::flush) when an automatic
433    ///   flush is triggered by reaching the chunk byte/row limit.
434    pub fn end_row(&mut self) -> Result<()> {
435        self.chunk.end_row()?;
436        self.row_count += 1;
437
438        // Auto-flush if we've reached chunk limits
439        if self.chunk.should_flush() {
440            self.flush()?;
441        }
442
443        Ok(())
444    }
445
446    /// Flushes the current buffer to the server.
447    ///
448    /// This sends all buffered rows as a chunk and resets the buffer.
449    /// Called automatically when chunk limits are reached.
450    ///
451    /// # Errors
452    ///
453    /// - Returns [`Error::FeatureNotSupported`] if the connection is using gRPC transport
454    ///   (COPY is TCP-only) and no COPY session exists yet.
455    /// - Returns [`Error::Server`] if the server rejects the `COPY IN` start
456    ///   or the subsequent data send.
457    /// - Returns [`Error::Io`] on transport-level I/O failures while writing
458    ///   the chunk.
459    pub fn flush(&mut self) -> Result<()> {
460        if self.chunk.is_empty() {
461            return Ok(());
462        }
463
464        let chunk_rows = self.chunk.row_count();
465        let Some(buffer) = self.chunk.take() else {
466            return Ok(());
467        };
468
469        // Ensure the COPY connection is started
470        if self.writer.is_none() {
471            let client = self.connection.tcp_client().ok_or_else(|| {
472                crate::Error::feature_not_supported(
473                    "Inserter requires a TCP connection. gRPC connections do not support COPY operations.",
474                )
475            })?;
476            let columns: Vec<&str> = self
477                .table_def
478                .columns
479                .iter()
480                .map(|c| c.name.as_str())
481                .collect();
482            let table_name = self.table_def.qualified_name();
483            self.writer = Some(client.copy_in(&table_name, &columns)?);
484        }
485
486        // Write the chunk directly to the socket, avoiding a full-chunk memcpy
487        // into the connection's write buffer. flush_stream ensures the data
488        // reaches the server before we return.
489        if let Some(ref mut writer) = self.writer {
490            writer.send_direct(&buffer)?;
491            writer.flush_stream()?;
492        }
493
494        debug!(
495            target: "hyperdb_api",
496            chunk = self.chunk_count,
497            rows = chunk_rows,
498            bytes = buffer.len(),
499            "inserter-chunk"
500        );
501
502        self.chunk_count += 1;
503        Ok(())
504    }
505
506    /// Adds a complete row of values.
507    ///
508    /// This is a convenience method that adds all column values at once
509    /// using the `IntoValue` trait for type-safe insertion.
510    ///
511    /// # Arguments
512    ///
513    /// * `values` - A slice of values implementing `IntoValue`.
514    ///
515    /// # Errors
516    ///
517    /// Returns an error if the number of values doesn't match the column count,
518    /// or if any value cannot be added.
519    ///
520    /// # Example
521    ///
522    /// ```no_run
523    /// use hyperdb_api::{Connection, CreateMode, Catalog, TableDefinition, Inserter, SqlType, Result};
524    ///
525    /// fn main() -> Result<()> {
526    ///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::CreateIfNotExists)?;
527    ///
528    ///     let table_def = TableDefinition::new("users")
529    ///         .add_required_column("id", SqlType::int())
530    ///         .add_nullable_column("name", SqlType::text());
531    ///
532    ///     Catalog::new(&conn).create_table(&table_def)?;
533    ///
534    ///     let mut inserter = Inserter::new(&conn, &table_def)?;
535    ///
536    ///     // Add rows using IntoValue trait
537    ///     inserter.add_row(&[&1i32, &"Alice"])?;
538    ///     inserter.add_row(&[&2i32, &"Bob"])?;
539    ///
540    ///     // Option<T> can be used for nullable columns
541    ///     inserter.add_row(&[&3i32, &None::<&str>])?;
542    ///
543    ///     let rows = inserter.execute()?;
544    ///     Ok(())
545    /// }
546    /// ```
547    pub fn add_row(&mut self, values: &[&dyn IntoValue]) -> Result<()> {
548        let column_count = self.table_def.column_count();
549        if values.len() != column_count {
550            return Err(Error::invalid_table_definition(format!(
551                "Column count mismatch: expected {} columns but got {}",
552                column_count,
553                values.len()
554            )));
555        }
556
557        for value in values {
558            value.add_to_inserter(self)?;
559        }
560
561        self.end_row()?;
562        Ok(())
563    }
564
565    /// Adds a Date value.
566    ///
567    /// # Errors
568    ///
569    /// See [`add_bool`](Self::add_bool).
570    #[inline]
571    pub fn add_date(&mut self, value: Date) -> Result<()> {
572        self.chunk.add_date(value)
573    }
574
575    /// Adds a Time value.
576    ///
577    /// # Errors
578    ///
579    /// See [`add_bool`](Self::add_bool).
580    #[inline]
581    pub fn add_time(&mut self, value: Time) -> Result<()> {
582        self.chunk.add_time(value)
583    }
584
585    /// Adds a Timestamp value.
586    ///
587    /// # Errors
588    ///
589    /// See [`add_bool`](Self::add_bool).
590    #[inline]
591    pub fn add_timestamp(&mut self, value: Timestamp) -> Result<()> {
592        self.chunk.add_timestamp(value)
593    }
594
595    /// Adds an `OffsetTimestamp` (TIMESTAMP WITH TIME ZONE) value.
596    ///
597    /// # Errors
598    ///
599    /// See [`add_bool`](Self::add_bool).
600    #[inline]
601    pub fn add_offset_timestamp(&mut self, value: OffsetTimestamp) -> Result<()> {
602        self.chunk.add_offset_timestamp(value)
603    }
604
605    /// Adds an Interval value.
606    ///
607    /// # Errors
608    ///
609    /// See [`add_bool`](Self::add_bool).
610    #[inline]
611    pub fn add_interval(&mut self, value: Interval) -> Result<()> {
612        self.chunk.add_interval(value)
613    }
614
615    /// Adds a Geography value.
616    ///
617    /// # Errors
618    ///
619    /// See [`add_bool`](Self::add_bool).
620    #[inline]
621    pub fn add_geography(&mut self, value: &Geography) -> Result<()> {
622        self.chunk.add_geography(value)
623    }
624
625    /// Adds a Numeric value.
626    ///
627    /// For NUMERIC(precision, scale) where precision ≤ [`Numeric::SMALL_NUMERIC_MAX_PRECISION`]
628    /// (18), the value is stored as i64. For higher precision, 128-bit storage is used.
629    ///
630    /// # Errors
631    ///
632    /// Returns an error if the column's precision cannot be determined from the
633    /// table definition. Ensure that NUMERIC columns are defined with explicit
634    /// `SqlType` information including precision.
635    pub fn add_numeric(&mut self, value: Numeric) -> Result<()> {
636        let column_index = self.chunk.column_index();
637
638        // Check the column's precision to determine storage format
639        let precision = self
640            .table_def
641            .columns
642            .get(column_index)
643            .and_then(super::table_definition::ColumnDefinition::sql_type)
644            .and_then(|t| t.precision())
645            .ok_or_else(|| {
646                let col_name = self
647                    .table_def
648                    .columns
649                    .get(column_index)
650                    .map_or("<unknown>", |c| c.name.as_str());
651                Error::conversion(format!(
652                    "Cannot determine numeric precision for column '{col_name}' at index {column_index}. \
653                     Ensure the column is defined with explicit SqlType including precision.\n\n\
654                     Example fix:\n  \
655                     table_def.add_column_with_type(\"{col_name}\", SqlType::Numeric {{ precision: 10, scale: 2 }}, true);"
656                ))
657            })?;
658
659        if precision <= Numeric::SMALL_NUMERIC_MAX_PRECISION {
660            // Small numeric: stored as i64
661            let unscaled = value.unscaled_value();
662            let narrowed = i64::try_from(unscaled).map_err(|_| {
663                Error::conversion(format!(
664                    "Numeric value {unscaled} is out of range for i64 storage (precision {precision})"
665                ))
666            })?;
667            self.chunk.add_i64(narrowed)
668        } else {
669            // Big numeric: stored as 128-bit
670            self.chunk.add_data128(&value.to_packed())
671        }
672    }
673
674    /// Executes the insert and commits all buffered rows.
675    ///
676    /// This sends any remaining buffered data and finishes the COPY operation.
677    /// Returns the number of rows inserted.
678    ///
679    /// The inserter is single-use: calling `execute` a second time returns
680    /// `Ok(0)` because the internal row counter has been reset and no further
681    /// data has been added. To insert additional batches, create a new
682    /// [`Inserter`].
683    ///
684    /// # Errors
685    ///
686    /// Returns an error if:
687    /// - There's an incomplete row (`column_index` != 0)
688    /// - The COPY connection fails to start
689    /// - Sending data fails
690    pub fn execute(&mut self) -> Result<u64> {
691        if self.chunk.column_index() != 0 {
692            return Err(Error::invalid_table_definition(
693                "Incomplete row at execute time",
694            ));
695        }
696
697        if self.row_count == 0 {
698            return Ok(0);
699        }
700
701        // Ensure COPY connection exists before proceeding when we have rows
702        if self.writer.is_none() {
703            let client = self.connection.tcp_client().ok_or_else(|| {
704                Error::feature_not_supported(
705                    "Inserter requires a TCP connection. gRPC connections do not support COPY operations.",
706                )
707            })?;
708            let columns: Vec<&str> = self
709                .table_def
710                .columns
711                .iter()
712                .map(|c| c.name.as_str())
713                .collect();
714            let table_name = self.table_def.qualified_name();
715            self.writer = Some(client.copy_in(&table_name, &columns)?);
716        }
717
718        // At this point, writer must exist since we have rows
719        let writer = self
720            .writer
721            .as_mut()
722            .ok_or_else(|| Error::internal("Failed to initialize COPY connection for inserter"))?;
723
724        // If we have buffered data that hasn't been sent yet
725        if !self.chunk.is_empty() {
726            writer.send(self.chunk.buffer())?;
727        }
728
729        // Write and send the COPY trailer
730        let mut trailer_buf = BytesMut::with_capacity(2);
731        copy::write_trailer(&mut trailer_buf);
732        writer.send(&trailer_buf)?;
733
734        // Finish the COPY operation
735        let rows = self
736            .writer
737            .take()
738            .map(hyperdb_api_core::client::CopyInWriter::finish)
739            .transpose()?
740            .unwrap_or(0);
741
742        let duration_ms = u64::try_from(self.start_time.elapsed().as_millis()).unwrap_or(u64::MAX);
743        info!(
744            target: "hyperdb_api",
745            rows,
746            chunks = self.chunk_count,
747            duration_ms,
748            table = %self.table_def.qualified_name(),
749            "inserter-end"
750        );
751
752        // Reset row counter so a stray second execute() call returns Ok(0)
753        // instead of attempting another COPY trailer on a finished writer.
754        self.row_count = 0;
755
756        Ok(rows)
757    }
758
759    /// Cancels the insert and discards all buffered rows.
760    pub fn cancel(&mut self) {
761        // Drop the in-progress writer (if any). The Drop impl on CopyInWriter
762        // sends a CopyFail to the server.
763        self.writer = None;
764        self.row_count = 0;
765    }
766}
767
768// =============================================================================
769// ColumnMapping
770// =============================================================================
771
772/// Defines how a column receives its value during insertion.
773///
774/// Column mappings allow you to:
775/// - Insert values directly from the inserter stream
776/// - Compute values using SQL expressions
777/// - Use server-side functions like `NOW()` or `DEFAULT`
778///
779/// # Example
780///
781/// ```
782/// use hyperdb_api::ColumnMapping;
783///
784/// // Simple column - insert value directly
785/// let id_col = ColumnMapping::new("id");
786///
787/// // Column with expression - computed value
788/// let created_at = ColumnMapping::with_expression("created_at", "NOW()");
789/// let full_name = ColumnMapping::with_expression("full_name", "first_name || ' ' || last_name");
790/// ```
791#[derive(Debug, Clone)]
792#[must_use = "ColumnMapping represents a column configuration that should not be discarded. Use it when defining inserter column mappings"]
793pub struct ColumnMapping {
794    /// The name of the target column.
795    pub column_name: String,
796    /// Optional SQL expression. If None, the value is inserted directly.
797    pub expression: Option<String>,
798}
799
800impl ColumnMapping {
801    /// Creates a column mapping for direct value insertion.
802    ///
803    /// The column will receive values directly from the inserter.
804    pub fn new(column_name: impl Into<String>) -> Self {
805        ColumnMapping {
806            column_name: column_name.into(),
807            expression: None,
808        }
809    }
810
811    /// Creates a column mapping with a SQL expression.
812    ///
813    /// The column value will be computed using the given SQL expression.
814    /// The expression can reference other columns or use SQL functions.
815    ///
816    /// # Arguments
817    ///
818    /// * `column_name` - The name of the target column.
819    /// * `expression` - A SQL expression to compute the column value.
820    ///
821    /// # Example
822    ///
823    /// ```
824    /// use hyperdb_api::ColumnMapping;
825    ///
826    /// // Use current timestamp
827    /// let created = ColumnMapping::with_expression("created_at", "NOW()");
828    ///
829    /// // Compute from other columns
830    /// let total = ColumnMapping::with_expression("total", "quantity * price");
831    /// ```
832    pub fn with_expression(column_name: impl Into<String>, expression: impl Into<String>) -> Self {
833        ColumnMapping {
834            column_name: column_name.into(),
835            expression: Some(expression.into()),
836        }
837    }
838
839    /// Returns the column name.
840    #[must_use]
841    pub fn column_name(&self) -> &str {
842        &self.column_name
843    }
844
845    /// Returns the SQL expression, if any.
846    #[must_use]
847    pub fn expression(&self) -> Option<&str> {
848        self.expression.as_deref()
849    }
850
851    /// Returns true if this is a direct value mapping (no expression).
852    #[must_use]
853    pub fn is_direct(&self) -> bool {
854        self.expression.is_none()
855    }
856
857    /// Returns the select list item for this mapping.
858    fn to_select_item(&self) -> String {
859        match &self.expression {
860            Some(expr) => format!("{} AS \"{}\"", expr, self.column_name.replace('"', "\"\"")),
861            None => format!("\"{}\"", self.column_name.replace('"', "\"\"")),
862        }
863    }
864}
865
866// =============================================================================
867// IntoValue Trait
868// =============================================================================
869
870/// Trait for types that can be inserted into a Hyper table.
871///
872/// This trait is implemented for common Rust types, allowing them to be
873/// used with [`Inserter::add_row()`] for type-safe insertion.
874///
875/// # Supported Types
876///
877/// - Integers: `i16`, `i32`, `i64`
878/// - Floats: `f32`, `f64`
879/// - `bool`
880/// - `&str`, `String`
881/// - `Option<T>` where `T: IntoValue` (for nullable columns)
882/// - Date/time types: `Date`, `Time`, `Timestamp`, `Interval`
883/// - `Numeric`, `Geography`, `Vec<u8>` (bytes)
884///
885/// # Example
886///
887/// ```no_run
888/// use hyperdb_api::{Connection, CreateMode, Catalog, TableDefinition, Inserter, IntoValue, SqlType, Result};
889///
890/// fn main() -> Result<()> {
891///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::CreateIfNotExists)?;
892///
893///     let table_def = TableDefinition::new("example")
894///         .add_required_column("a", SqlType::int())
895///         .add_nullable_column("b", SqlType::text())
896///         .add_nullable_column("c", SqlType::double());
897///     Catalog::new(&conn).create_table(&table_def)?;
898///
899///     let mut inserter = Inserter::new(&conn, &table_def)?;
900///
901///     // IntoValue allows adding rows with mixed types
902///     inserter.add_row(&[&1i32, &"Alice", &Some(3.14f64)])?;
903///     inserter.add_row(&[&2i32, &"Bob", &None::<f64>])?; // NULL value
904///
905///     inserter.execute()?;
906///     Ok(())
907/// }
908/// ```
909pub trait IntoValue {
910    /// Adds this value to the inserter.
911    ///
912    /// # Errors
913    ///
914    /// Implementations call the matching `Inserter::add_*` method and
915    /// forward its error — see [`Inserter::add_bool`] for the shared
916    /// failure modes (too many columns, NULL into non-nullable, etc).
917    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()>;
918}
919
920// Implementations for basic types
921
922impl IntoValue for bool {
923    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
924        inserter.add_bool(*self)
925    }
926}
927
928impl IntoValue for i16 {
929    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
930        inserter.add_i16(*self)
931    }
932}
933
934impl IntoValue for i32 {
935    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
936        inserter.add_i32(*self)
937    }
938}
939
940impl IntoValue for i64 {
941    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
942        inserter.add_i64(*self)
943    }
944}
945
946impl IntoValue for f32 {
947    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
948        inserter.add_f32(*self)
949    }
950}
951
952impl IntoValue for f64 {
953    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
954        inserter.add_f64(*self)
955    }
956}
957
958impl IntoValue for str {
959    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
960        inserter.add_str(self)
961    }
962}
963
964impl IntoValue for String {
965    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
966        inserter.add_str(self)
967    }
968}
969
970impl IntoValue for [u8] {
971    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
972        inserter.add_bytes(self)
973    }
974}
975
976impl IntoValue for Vec<u8> {
977    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
978        inserter.add_bytes(self)
979    }
980}
981
982// Hyper-specific types
983
984impl IntoValue for Date {
985    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
986        inserter.add_date(*self)
987    }
988}
989
990impl IntoValue for Time {
991    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
992        inserter.add_time(*self)
993    }
994}
995
996impl IntoValue for Timestamp {
997    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
998        inserter.add_timestamp(*self)
999    }
1000}
1001
1002impl IntoValue for OffsetTimestamp {
1003    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1004        inserter.add_offset_timestamp(*self)
1005    }
1006}
1007
1008impl IntoValue for Interval {
1009    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1010        inserter.add_interval(*self)
1011    }
1012}
1013
1014impl IntoValue for Numeric {
1015    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1016        inserter.add_numeric(*self)
1017    }
1018}
1019
1020impl IntoValue for Geography {
1021    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1022        inserter.add_geography(self)
1023    }
1024}
1025
1026// Option<T> for nullable values
1027impl<T: IntoValue> IntoValue for Option<T> {
1028    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1029        match self {
1030            Some(value) => value.add_to_inserter(inserter),
1031            None => inserter.add_null(),
1032        }
1033    }
1034}
1035
1036// Reference implementations for primitives
1037
1038impl IntoValue for &bool {
1039    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1040        inserter.add_bool(**self)
1041    }
1042}
1043
1044impl IntoValue for &i16 {
1045    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1046        inserter.add_i16(**self)
1047    }
1048}
1049
1050impl IntoValue for &i32 {
1051    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1052        inserter.add_i32(**self)
1053    }
1054}
1055
1056impl IntoValue for &i64 {
1057    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1058        inserter.add_i64(**self)
1059    }
1060}
1061
1062impl IntoValue for &f32 {
1063    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1064        inserter.add_f32(**self)
1065    }
1066}
1067
1068impl IntoValue for &f64 {
1069    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1070        inserter.add_f64(**self)
1071    }
1072}
1073
1074impl IntoValue for &String {
1075    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1076        inserter.add_str(self)
1077    }
1078}
1079
1080impl IntoValue for &str {
1081    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1082        inserter.add_str(self)
1083    }
1084}
1085
1086impl IntoValue for &&str {
1087    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1088        inserter.add_str(self)
1089    }
1090}
1091
1092impl IntoValue for &[u8] {
1093    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1094        inserter.add_bytes(self)
1095    }
1096}
1097
1098impl IntoValue for &Date {
1099    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1100        inserter.add_date(**self)
1101    }
1102}
1103
1104impl IntoValue for &Time {
1105    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1106        inserter.add_time(**self)
1107    }
1108}
1109
1110impl IntoValue for &Timestamp {
1111    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1112        inserter.add_timestamp(**self)
1113    }
1114}
1115
1116impl IntoValue for &OffsetTimestamp {
1117    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1118        inserter.add_offset_timestamp(**self)
1119    }
1120}
1121
1122impl IntoValue for &Interval {
1123    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1124        inserter.add_interval(**self)
1125    }
1126}
1127
1128impl IntoValue for &Numeric {
1129    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1130        inserter.add_numeric(**self)
1131    }
1132}
1133
1134impl IntoValue for &Geography {
1135    fn add_to_inserter(&self, inserter: &mut Inserter<'_>) -> Result<()> {
1136        inserter.add_geography(self)
1137    }
1138}
1139
1140// =============================================================================
1141// MappedInserter
1142// =============================================================================
1143
1144/// An inserter that supports SQL expression mappings.
1145///
1146/// This inserter uses a staging table to support computed columns via
1147/// INSERT...SELECT with SQL expressions. It's created by
1148/// [`Inserter::with_column_mappings`].
1149#[derive(Debug)]
1150pub struct MappedInserter<'conn> {
1151    /// The underlying inserter for the staging table.
1152    inner: Inserter<'conn>,
1153    /// The target table name.
1154    target_table: crate::TableName,
1155    /// The column mappings.
1156    mappings: Vec<ColumnMapping>,
1157    /// The staging table name.
1158    staging_table: String,
1159}
1160
1161impl<'conn> MappedInserter<'conn> {
1162    /// Creates a new mapped inserter.
1163    fn new<T>(
1164        connection: &'conn Connection,
1165        inserter_def: &TableDefinition,
1166        target_table: T,
1167        mappings: &[ColumnMapping],
1168    ) -> Result<Self>
1169    where
1170        T: TryInto<crate::TableName>,
1171        crate::Error: From<T::Error>,
1172    {
1173        let target_table = target_table.try_into()?;
1174
1175        // Create a unique staging table name
1176        let staging_table = format!("_hyper_staging_{}", std::process::id());
1177
1178        // Create the staging table definition (temporary)
1179        let mut staging_def = inserter_def.clone();
1180        staging_def.name.clone_from(&staging_table);
1181
1182        // Create the staging table
1183        let create_sql = staging_def.to_create_sql(true)?;
1184        let create_temp = create_sql.replace("CREATE TABLE", "CREATE TEMPORARY TABLE");
1185        connection.execute_command(&create_temp)?;
1186
1187        // Create the inner inserter for the staging table
1188        let inner = Inserter::new(connection, &staging_def)?;
1189
1190        Ok(MappedInserter {
1191            inner,
1192            target_table,
1193            mappings: mappings.to_vec(),
1194            staging_table,
1195        })
1196    }
1197
1198    /// Adds a row of values to the inserter.
1199    ///
1200    /// The values should correspond to the columns in the inserter definition,
1201    /// not the target table.
1202    ///
1203    /// # Errors
1204    ///
1205    /// Forwards the error from [`Inserter::add_row`].
1206    pub fn add_row(&mut self, values: &[&dyn IntoValue]) -> Result<()> {
1207        self.inner.add_row(values)
1208    }
1209
1210    /// Adds a NULL value.
1211    ///
1212    /// # Errors
1213    ///
1214    /// Forwards the error from [`Inserter::add_null`].
1215    pub fn add_null(&mut self) -> Result<()> {
1216        self.inner.add_null()
1217    }
1218
1219    /// Adds a boolean value.
1220    ///
1221    /// # Errors
1222    ///
1223    /// Forwards the error from [`Inserter::add_bool`].
1224    pub fn add_bool(&mut self, value: bool) -> Result<()> {
1225        self.inner.add_bool(value)
1226    }
1227
1228    /// Adds an i16 value.
1229    ///
1230    /// # Errors
1231    ///
1232    /// Forwards the error from [`Inserter::add_i16`].
1233    pub fn add_i16(&mut self, value: i16) -> Result<()> {
1234        self.inner.add_i16(value)
1235    }
1236
1237    /// Adds an i32 value.
1238    ///
1239    /// # Errors
1240    ///
1241    /// Forwards the error from [`Inserter::add_i32`].
1242    pub fn add_i32(&mut self, value: i32) -> Result<()> {
1243        self.inner.add_i32(value)
1244    }
1245
1246    /// Adds an i64 value.
1247    ///
1248    /// # Errors
1249    ///
1250    /// Forwards the error from [`Inserter::add_i64`].
1251    pub fn add_i64(&mut self, value: i64) -> Result<()> {
1252        self.inner.add_i64(value)
1253    }
1254
1255    /// Adds an f32 value.
1256    ///
1257    /// # Errors
1258    ///
1259    /// Forwards the error from [`Inserter::add_f32`].
1260    pub fn add_f32(&mut self, value: f32) -> Result<()> {
1261        self.inner.add_f32(value)
1262    }
1263
1264    /// Adds an f64 value.
1265    ///
1266    /// # Errors
1267    ///
1268    /// Forwards the error from [`Inserter::add_f64`].
1269    pub fn add_f64(&mut self, value: f64) -> Result<()> {
1270        self.inner.add_f64(value)
1271    }
1272
1273    /// Adds a string value.
1274    ///
1275    /// # Errors
1276    ///
1277    /// Forwards the error from [`Inserter::add_str`].
1278    pub fn add_str(&mut self, value: &str) -> Result<()> {
1279        self.inner.add_str(value)
1280    }
1281
1282    /// Adds a bytes value.
1283    ///
1284    /// # Errors
1285    ///
1286    /// Forwards the error from [`Inserter::add_bytes`].
1287    pub fn add_bytes(&mut self, value: &[u8]) -> Result<()> {
1288        self.inner.add_bytes(value)
1289    }
1290
1291    /// Ends the current row.
1292    ///
1293    /// # Errors
1294    ///
1295    /// Forwards the error from [`Inserter::end_row`].
1296    pub fn end_row(&mut self) -> Result<()> {
1297        self.inner.end_row()
1298    }
1299
1300    /// Executes the insert with column mappings.
1301    ///
1302    /// This method:
1303    /// 1. Inserts all buffered rows into the staging table
1304    /// 2. Executes INSERT...SELECT from staging to target with mappings
1305    /// 3. Drops the staging table
1306    ///
1307    /// Returns the number of rows inserted into the target table.
1308    ///
1309    /// # Errors
1310    ///
1311    /// - Returns the error from the inner [`Inserter::execute`] if writing
1312    ///   the staging rows fails.
1313    /// - Returns [`Error::Server`] if the `INSERT ... SELECT` from staging
1314    ///   to the target table is rejected (e.g. a mapping expression fails
1315    ///   to evaluate).
1316    /// - Returns [`Error::Server`] if dropping the staging table fails.
1317    pub fn execute(&mut self) -> Result<u64> {
1318        let connection = self.inner.connection;
1319        let staging_table = self.staging_table.clone();
1320
1321        // Insert data into staging table
1322        let _staging_rows = self.inner.execute()?;
1323
1324        // Build the INSERT...SELECT statement
1325        use hyperdb_api_core::protocol::escape::SqlIdentifier;
1326
1327        let target_columns: Vec<String> = self
1328            .mappings
1329            .iter()
1330            .map(|m| format!("{}", SqlIdentifier(&m.column_name)))
1331            .collect();
1332
1333        let select_items: Vec<String> = self
1334            .mappings
1335            .iter()
1336            .map(ColumnMapping::to_select_item)
1337            .collect();
1338
1339        let sql = format!(
1340            "INSERT INTO {} ({}) SELECT {} FROM {}",
1341            self.target_table,
1342            target_columns.join(", "),
1343            select_items.join(", "),
1344            SqlIdentifier(&staging_table),
1345        );
1346
1347        // Execute the INSERT...SELECT (returns row count directly)
1348        let row_count = connection.execute_command(&sql)?;
1349
1350        // Drop the staging table
1351        connection.execute_command(&format!(
1352            "DROP TABLE IF EXISTS {}",
1353            SqlIdentifier(&staging_table)
1354        ))?;
1355
1356        // Return the number of rows inserted
1357        Ok(row_count)
1358    }
1359
1360    /// Cancels the insert and drops the staging table.
1361    ///
1362    /// This method handles cleanup failures gracefully by logging warnings
1363    /// instead of returning errors. This prevents masking the original error
1364    /// that caused the cancellation.
1365    ///
1366    /// # Logging
1367    ///
1368    /// Cleanup failures are logged using the `tracing` crate at WARN level.
1369    /// If `tracing` is not initialized, errors are written to stderr.
1370    pub fn cancel(&mut self) {
1371        let connection = self.inner.connection;
1372        let staging_table = &self.staging_table;
1373
1374        // Drop the staging table, but don't fail if cleanup fails
1375        // This avoids masking the original error that caused cancellation
1376        if let Err(e) = connection.execute_command(&format!(
1377            "DROP TABLE IF EXISTS \"{}\"",
1378            staging_table.replace('"', "\"\"")
1379        )) {
1380            // Log the cleanup failure for debugging
1381            // In production, consider using a logging framework like `tracing`
1382            eprintln!("Warning: Failed to drop staging table '{staging_table}' during cancel: {e}");
1383        }
1384    }
1385}
1386
1387// =============================================================================
1388// InsertChunk - Thread-safe chunk for parallel encoding
1389// =============================================================================
1390
1391/// A thread-safe chunk for encoding rows in parallel.
1392///
1393/// `InsertChunk` can be created and populated in any thread, then sent to a
1394/// [`ChunkSender`] for transmission. This enables parallel data encoding across
1395/// multiple worker threads while serializing the actual network sends.
1396///
1397/// # Example
1398///
1399/// ```no_run
1400/// use hyperdb_api::{InsertChunk, TableDefinition, SqlType, Result};
1401///
1402/// fn encode_chunk(table_def: &TableDefinition, start_id: i32) -> Result<InsertChunk> {
1403///     let mut chunk = InsertChunk::from_table_definition(table_def);
1404///     
1405///     for i in 0..1000 {
1406///         chunk.add_i32(start_id + i)?;
1407///         chunk.add_str(&format!("Item {}", start_id + i))?;
1408///         chunk.end_row()?;
1409///     }
1410///     
1411///     Ok(chunk)
1412/// }
1413/// ```
1414#[derive(Debug)]
1415pub struct InsertChunk {
1416    buffer: BytesMut,
1417    header_written: bool,
1418    column_index: usize,
1419    column_count: usize,
1420    row_count: usize,
1421    column_nullable: Vec<bool>,
1422}
1423
1424// SAFETY: Every field of `InsertChunk` (`BytesMut`, `bool`, `usize`,
1425// `Vec<bool>`) is itself `Send`, and none of them hold raw pointers or
1426// thread-local state. The manual `unsafe impl` exists only because the
1427// auto-trait derivation is conservative for this struct's compilation context;
1428// the compound type has no `!Send` components.
1429unsafe impl Send for InsertChunk {}
1430// SAFETY: Same reasoning as the `Send` impl above — all fields are `Sync`
1431// and there is no interior mutability crossing a `&InsertChunk` boundary,
1432// so sharing `&InsertChunk` across threads is sound.
1433unsafe impl Sync for InsertChunk {}
1434
1435impl InsertChunk {
1436    /// Creates a new empty chunk with the given schema.
1437    ///
1438    /// # Arguments
1439    ///
1440    /// * `column_count` - Number of columns per row
1441    /// * `column_nullable` - Whether each column is nullable
1442    #[must_use]
1443    pub fn new(column_count: usize, column_nullable: Vec<bool>) -> Self {
1444        debug_assert_eq!(column_count, column_nullable.len());
1445        InsertChunk {
1446            buffer: BytesMut::with_capacity(INITIAL_BUFFER_SIZE),
1447            header_written: false,
1448            column_index: 0,
1449            column_count,
1450            row_count: 0,
1451            column_nullable,
1452        }
1453    }
1454
1455    /// Creates a chunk from a table definition.
1456    #[must_use]
1457    pub fn from_table_definition(table_def: &TableDefinition) -> Self {
1458        let column_nullable: Vec<bool> = table_def.columns.iter().map(|c| c.nullable).collect();
1459        Self::new(table_def.column_count(), column_nullable)
1460    }
1461
1462    /// Returns the number of complete rows in this chunk.
1463    #[must_use]
1464    pub fn row_count(&self) -> usize {
1465        self.row_count
1466    }
1467
1468    /// Returns the current buffer size in bytes.
1469    #[must_use]
1470    pub fn buffer_size(&self) -> usize {
1471        self.buffer.len()
1472    }
1473
1474    /// Returns true if the chunk has reached size or row limits and should be sent.
1475    #[must_use]
1476    pub fn should_flush(&self) -> bool {
1477        self.row_count >= CHUNK_ROW_LIMIT || self.buffer.len() >= CHUNK_SIZE_LIMIT
1478    }
1479
1480    /// Returns true if the chunk is empty (no rows).
1481    #[must_use]
1482    pub fn is_empty(&self) -> bool {
1483        self.row_count == 0
1484    }
1485
1486    /// Takes the buffer, consuming the chunk data.
1487    ///
1488    /// Returns `None` if the chunk is empty. After calling this, the chunk
1489    /// can be reused by calling the add_* methods again.
1490    ///
1491    /// Note: The header flag is NOT reset - subsequent chunks from the same
1492    /// `InsertChunk` will NOT include the header (`HyperBinary` only needs one
1493    /// header per COPY stream).
1494    pub fn take(&mut self) -> Option<BytesMut> {
1495        if self.row_count == 0 {
1496            return None;
1497        }
1498        // Don't reset header_written - only first chunk should have header
1499        self.row_count = 0;
1500        Some(std::mem::take(&mut self.buffer))
1501    }
1502
1503    /// Resets the chunk for reuse without reallocating.
1504    pub fn clear(&mut self) {
1505        self.buffer.clear();
1506        self.header_written = false;
1507        self.column_index = 0;
1508        self.row_count = 0;
1509    }
1510
1511    #[allow(
1512        clippy::inline_always,
1513        reason = "hot-path numeric kernel; forced inlining measured to matter on this specific function"
1514    )]
1515    fn ensure_header(&mut self) {
1516        if !self.header_written {
1517            copy::write_header(&mut self.buffer);
1518            self.header_written = true;
1519        }
1520    }
1521
1522    #[expect(
1523        clippy::inline_always,
1524        reason = "hot inner loop of the inserter; measured to matter for per-row throughput"
1525    )]
1526    #[inline(always)]
1527    fn current_column_nullable(&self) -> bool {
1528        *self.column_nullable.get(self.column_index).unwrap_or(&true)
1529    }
1530
1531    /// Adds a NULL value for the current column.
1532    ///
1533    /// # Errors
1534    ///
1535    /// - Returns [`Error::InvalidTableDefinition`] with message `"Too many columns in row"`
1536    ///   if the current row already has all columns supplied.
1537    /// - Returns [`Error::InvalidTableDefinition`] with message
1538    ///   `"Cannot add NULL to non-nullable column"` if the current column
1539    ///   is `NOT NULL` in the schema.
1540    pub fn add_null(&mut self) -> Result<()> {
1541        if self.column_index >= self.column_count {
1542            return Err(Error::invalid_table_definition("Too many columns in row"));
1543        }
1544        if !self.current_column_nullable() {
1545            return Err(Error::invalid_table_definition(
1546                "Cannot add NULL to non-nullable column",
1547            ));
1548        }
1549        self.ensure_header();
1550        copy::write_null(&mut self.buffer);
1551        self.column_index += 1;
1552        Ok(())
1553    }
1554
1555    /// Adds a boolean value.
1556    ///
1557    /// # Errors
1558    ///
1559    /// Returns [`Error::InvalidTableDefinition`] with message `"Too many columns in row"` if
1560    /// the current row already has all columns supplied.
1561    pub fn add_bool(&mut self, value: bool) -> Result<()> {
1562        if self.column_index >= self.column_count {
1563            return Err(Error::invalid_table_definition("Too many columns in row"));
1564        }
1565        self.ensure_header();
1566        let int_value = i8::from(value);
1567        if self.current_column_nullable() {
1568            copy::write_i8(&mut self.buffer, int_value);
1569        } else {
1570            copy::write_i8_not_null(&mut self.buffer, int_value);
1571        }
1572        self.column_index += 1;
1573        Ok(())
1574    }
1575
1576    /// Adds an i16 value (SMALLINT).
1577    ///
1578    /// # Errors
1579    ///
1580    /// See [`add_bool`](Self::add_bool).
1581    pub fn add_i16(&mut self, value: i16) -> Result<()> {
1582        if self.column_index >= self.column_count {
1583            return Err(Error::invalid_table_definition("Too many columns in row"));
1584        }
1585        self.ensure_header();
1586        if self.current_column_nullable() {
1587            copy::write_i16(&mut self.buffer, value);
1588        } else {
1589            copy::write_i16_not_null(&mut self.buffer, value);
1590        }
1591        self.column_index += 1;
1592        Ok(())
1593    }
1594
1595    /// Adds an i32 value (INT).
1596    ///
1597    /// # Errors
1598    ///
1599    /// See [`add_bool`](Self::add_bool).
1600    pub fn add_i32(&mut self, value: i32) -> Result<()> {
1601        if self.column_index >= self.column_count {
1602            return Err(Error::invalid_table_definition("Too many columns in row"));
1603        }
1604        self.ensure_header();
1605        if self.current_column_nullable() {
1606            copy::write_i32(&mut self.buffer, value);
1607        } else {
1608            copy::write_i32_not_null(&mut self.buffer, value);
1609        }
1610        self.column_index += 1;
1611        Ok(())
1612    }
1613
1614    /// Adds an i64 value (BIGINT).
1615    ///
1616    /// # Errors
1617    ///
1618    /// See [`add_bool`](Self::add_bool).
1619    pub fn add_i64(&mut self, value: i64) -> Result<()> {
1620        if self.column_index >= self.column_count {
1621            return Err(Error::invalid_table_definition("Too many columns in row"));
1622        }
1623        self.ensure_header();
1624        if self.current_column_nullable() {
1625            copy::write_i64(&mut self.buffer, value);
1626        } else {
1627            copy::write_i64_not_null(&mut self.buffer, value);
1628        }
1629        self.column_index += 1;
1630        Ok(())
1631    }
1632
1633    /// Adds an f32 value (REAL/FLOAT4).
1634    ///
1635    /// # Errors
1636    ///
1637    /// See [`add_bool`](Self::add_bool).
1638    pub fn add_f32(&mut self, value: f32) -> Result<()> {
1639        if self.column_index >= self.column_count {
1640            return Err(Error::invalid_table_definition("Too many columns in row"));
1641        }
1642        self.ensure_header();
1643        if self.current_column_nullable() {
1644            copy::write_f32(&mut self.buffer, value);
1645        } else {
1646            copy::write_f32_not_null(&mut self.buffer, value);
1647        }
1648        self.column_index += 1;
1649        Ok(())
1650    }
1651
1652    /// Adds an f64 value (DOUBLE PRECISION/FLOAT8).
1653    ///
1654    /// # Errors
1655    ///
1656    /// See [`add_bool`](Self::add_bool).
1657    pub fn add_f64(&mut self, value: f64) -> Result<()> {
1658        if self.column_index >= self.column_count {
1659            return Err(Error::invalid_table_definition("Too many columns in row"));
1660        }
1661        self.ensure_header();
1662        if self.current_column_nullable() {
1663            copy::write_f64(&mut self.buffer, value);
1664        } else {
1665            copy::write_f64_not_null(&mut self.buffer, value);
1666        }
1667        self.column_index += 1;
1668        Ok(())
1669    }
1670
1671    /// Adds a string value (TEXT/VARCHAR).
1672    ///
1673    /// # Errors
1674    ///
1675    /// See [`add_bool`](Self::add_bool).
1676    pub fn add_str(&mut self, value: &str) -> Result<()> {
1677        self.add_bytes(value.as_bytes())
1678    }
1679
1680    /// Adds a bytes value (BYTEA).
1681    ///
1682    /// # Errors
1683    ///
1684    /// See [`add_bool`](Self::add_bool).
1685    pub fn add_bytes(&mut self, value: &[u8]) -> Result<()> {
1686        if self.column_index >= self.column_count {
1687            return Err(Error::invalid_table_definition("Too many columns in row"));
1688        }
1689        if value.len() > u32::MAX as usize {
1690            return Err(Error::conversion(format!(
1691                "Value length {} exceeds HyperBinary 4-byte length limit ({})",
1692                value.len(),
1693                u32::MAX
1694            )));
1695        }
1696        self.ensure_header();
1697        if self.current_column_nullable() {
1698            copy::write_varbinary(&mut self.buffer, value);
1699        } else {
1700            copy::write_varbinary_not_null(&mut self.buffer, value);
1701        }
1702        self.column_index += 1;
1703        Ok(())
1704    }
1705
1706    /// Adds a 128-bit value (NUMERIC/INTERVAL).
1707    ///
1708    /// # Errors
1709    ///
1710    /// See [`add_bool`](Self::add_bool).
1711    pub fn add_data128(&mut self, value: &[u8; 16]) -> Result<()> {
1712        if self.column_index >= self.column_count {
1713            return Err(Error::invalid_table_definition("Too many columns in row"));
1714        }
1715        self.ensure_header();
1716        if self.current_column_nullable() {
1717            copy::write_data128(&mut self.buffer, value);
1718        } else {
1719            copy::write_data128_not_null(&mut self.buffer, value);
1720        }
1721        self.column_index += 1;
1722        Ok(())
1723    }
1724
1725    /// Adds a Date value.
1726    ///
1727    /// # Errors
1728    ///
1729    /// See [`add_bool`](Self::add_bool).
1730    pub fn add_date(&mut self, value: Date) -> Result<()> {
1731        if self.column_index >= self.column_count {
1732            return Err(Error::invalid_table_definition("Too many columns in row"));
1733        }
1734        self.ensure_header();
1735        let julian_day = value.to_julian_day();
1736        if self.current_column_nullable() {
1737            copy::write_i32(&mut self.buffer, julian_day);
1738        } else {
1739            copy::write_i32_not_null(&mut self.buffer, julian_day);
1740        }
1741        self.column_index += 1;
1742        Ok(())
1743    }
1744
1745    /// Adds a Time value.
1746    ///
1747    /// # Errors
1748    ///
1749    /// See [`add_bool`](Self::add_bool).
1750    pub fn add_time(&mut self, value: Time) -> Result<()> {
1751        if self.column_index >= self.column_count {
1752            return Err(Error::invalid_table_definition("Too many columns in row"));
1753        }
1754        self.ensure_header();
1755        let micros = value.to_microseconds();
1756        if self.current_column_nullable() {
1757            copy::write_i64(&mut self.buffer, micros);
1758        } else {
1759            copy::write_i64_not_null(&mut self.buffer, micros);
1760        }
1761        self.column_index += 1;
1762        Ok(())
1763    }
1764
1765    /// Adds a Timestamp value.
1766    ///
1767    /// # Errors
1768    ///
1769    /// See [`add_bool`](Self::add_bool).
1770    pub fn add_timestamp(&mut self, value: Timestamp) -> Result<()> {
1771        if self.column_index >= self.column_count {
1772            return Err(Error::invalid_table_definition("Too many columns in row"));
1773        }
1774        self.ensure_header();
1775        let micros = value.to_microseconds();
1776        if self.current_column_nullable() {
1777            copy::write_i64(&mut self.buffer, micros);
1778        } else {
1779            copy::write_i64_not_null(&mut self.buffer, micros);
1780        }
1781        self.column_index += 1;
1782        Ok(())
1783    }
1784
1785    /// Adds an `OffsetTimestamp` (TIMESTAMP WITH TIME ZONE) value.
1786    ///
1787    /// # Errors
1788    ///
1789    /// See [`add_bool`](Self::add_bool).
1790    pub fn add_offset_timestamp(&mut self, value: OffsetTimestamp) -> Result<()> {
1791        if self.column_index >= self.column_count {
1792            return Err(Error::invalid_table_definition("Too many columns in row"));
1793        }
1794        self.ensure_header();
1795        let micros = value.to_microseconds_utc();
1796        if self.current_column_nullable() {
1797            copy::write_i64(&mut self.buffer, micros);
1798        } else {
1799            copy::write_i64_not_null(&mut self.buffer, micros);
1800        }
1801        self.column_index += 1;
1802        Ok(())
1803    }
1804
1805    /// Adds an Interval value.
1806    ///
1807    /// # Errors
1808    ///
1809    /// See [`add_bool`](Self::add_bool).
1810    pub fn add_interval(&mut self, value: Interval) -> Result<()> {
1811        if self.column_index >= self.column_count {
1812            return Err(Error::invalid_table_definition("Too many columns in row"));
1813        }
1814        self.ensure_header();
1815        let packed = value.to_packed();
1816        if self.current_column_nullable() {
1817            copy::write_data128(&mut self.buffer, &packed);
1818        } else {
1819            copy::write_data128_not_null(&mut self.buffer, &packed);
1820        }
1821        self.column_index += 1;
1822        Ok(())
1823    }
1824
1825    /// Adds a Geography value.
1826    ///
1827    /// # Errors
1828    ///
1829    /// See [`add_bool`](Self::add_bool).
1830    pub fn add_geography(&mut self, value: &Geography) -> Result<()> {
1831        // Geography uses the same varbinary path as add_bytes
1832        self.add_bytes(value.as_bytes())
1833    }
1834
1835    /// Ends the current row.
1836    ///
1837    /// Returns an error if the wrong number of columns were added.
1838    ///
1839    /// # Errors
1840    ///
1841    /// Returns [`Error::InvalidTableDefinition`] if fewer (or more) columns were supplied
1842    /// for this row than the chunk's column count.
1843    pub fn end_row(&mut self) -> Result<()> {
1844        if self.column_index != self.column_count {
1845            return Err(Error::invalid_table_definition(format!(
1846                "Expected {} columns, got {}",
1847                self.column_count, self.column_index
1848            )));
1849        }
1850        self.column_index = 0;
1851        self.row_count += 1;
1852        Ok(())
1853    }
1854
1855    /// Returns the current column index (for checking incomplete rows).
1856    #[must_use]
1857    pub fn column_index(&self) -> usize {
1858        self.column_index
1859    }
1860
1861    /// Returns a reference to the internal buffer.
1862    pub(crate) fn buffer(&self) -> &BytesMut {
1863        &self.buffer
1864    }
1865}
1866
1867// =============================================================================
1868// ChunkSender - Mutex-protected sender for InsertChunks
1869// =============================================================================
1870
1871use std::sync::Mutex;
1872use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
1873
1874/// A thread-safe sender for [`InsertChunk`]s.
1875///
1876/// `ChunkSender` manages the COPY protocol connection and ensures that only one
1877/// chunk is sent at a time. Multiple threads can call `send_chunk()` concurrently;
1878/// the mutex ensures serialized access.
1879///
1880/// # Example
1881///
1882/// ```no_run
1883/// use hyperdb_api::{Catalog, Connection, CreateMode, ChunkSender, InsertChunk, TableDefinition, SqlType, Result};
1884/// use std::sync::mpsc;
1885/// use std::thread;
1886///
1887/// fn main() -> Result<()> {
1888///     let conn = Connection::connect("localhost:7483", "test.hyper", CreateMode::CreateIfNotExists)?;
1889///     
1890///     let table_def = TableDefinition::new("products")
1891///         .add_required_column("id", SqlType::int())
1892///         .add_nullable_column("name", SqlType::text());
1893///     
1894///     Catalog::new(&conn).create_table(&table_def)?;
1895///     
1896///     let sender = ChunkSender::new(&conn, &table_def)?;
1897///     let (tx, rx) = mpsc::channel::<InsertChunk>();
1898///     
1899///     // Worker thread
1900///     let table_def_clone = table_def.clone();
1901///     let handle = thread::spawn(move || {
1902///         let mut chunk = InsertChunk::from_table_definition(&table_def_clone);
1903///         for i in 0..1000i32 {
1904///             chunk.add_i32(i).unwrap();
1905///             chunk.add_str(&format!("Product {}", i)).unwrap();
1906///             chunk.end_row().unwrap();
1907///         }
1908///         tx.send(chunk).unwrap();
1909///     });
1910///     
1911///     // Receive and send chunks
1912///     while let Ok(chunk) = rx.recv() {
1913///         sender.send_chunk(chunk)?;
1914///     }
1915///     
1916///     handle.join().unwrap();
1917///     let rows = sender.finish()?;
1918///     println!("Inserted {} rows", rows);
1919///     Ok(())
1920/// }
1921/// ```
1922#[derive(Debug)]
1923pub struct ChunkSender<'conn> {
1924    connection: &'conn Connection,
1925    table_name: String,
1926    columns: Vec<String>,
1927    writer: Mutex<Option<CopyInWriter<'conn>>>,
1928    header_sent: std::sync::atomic::AtomicBool,
1929    total_rows: AtomicU64,
1930    chunks_sent: AtomicUsize,
1931}
1932
1933impl<'conn> ChunkSender<'conn> {
1934    /// Creates a new chunk sender for the given table.
1935    ///
1936    /// # Errors
1937    ///
1938    /// Returns [`Error::InvalidTableDefinition`] if `table_def` has zero
1939    /// columns. The COPY session itself is opened lazily on the first
1940    /// [`send_chunk`](Self::send_chunk), so transport errors surface there.
1941    pub fn new(connection: &'conn Connection, table_def: &TableDefinition) -> Result<Self> {
1942        if table_def.column_count() == 0 {
1943            return Err(Error::invalid_table_definition(
1944                "Table definition must have at least one column",
1945            ));
1946        }
1947
1948        let columns: Vec<String> = table_def.columns.iter().map(|c| c.name.clone()).collect();
1949        let table_name = table_def.qualified_name();
1950
1951        Ok(ChunkSender {
1952            connection,
1953            table_name,
1954            columns,
1955            writer: Mutex::new(None),
1956            header_sent: std::sync::atomic::AtomicBool::new(false),
1957            total_rows: AtomicU64::new(0),
1958            chunks_sent: AtomicUsize::new(0),
1959        })
1960    }
1961
1962    /// Sends a chunk to Hyper.
1963    ///
1964    /// This method is thread-safe - multiple threads can call it concurrently,
1965    /// but only one chunk will be sent at a time.
1966    ///
1967    /// Each `InsertChunk` includes a `HyperBinary` header (19 bytes). This method
1968    /// automatically handles headers: the first chunk's header is sent, and
1969    /// headers in subsequent chunks are stripped (`HyperBinary` expects only one
1970    /// header per COPY stream).
1971    ///
1972    /// # Errors
1973    ///
1974    /// Returns an error if the chunk is empty or if sending fails.
1975    pub fn send_chunk(&self, mut chunk: InsertChunk) -> Result<()> {
1976        // Capture row count before take() resets it
1977        let row_count = chunk.row_count();
1978
1979        let Some(buffer) = chunk.take() else {
1980            return Ok(());
1981        };
1982
1983        // Acquire the lock for exclusive send access
1984        let mut writer_guard = self
1985            .writer
1986            .lock()
1987            .map_err(|_| Error::internal("ChunkSender mutex poisoned"))?;
1988
1989        // Lazily initialize the COPY connection
1990        if writer_guard.is_none() {
1991            let client = self.connection.tcp_client().ok_or_else(|| {
1992                Error::feature_not_supported(
1993                    "ChunkSender requires a TCP connection. gRPC connections do not support COPY operations."
1994                )
1995            })?;
1996            let columns: Vec<&str> = self
1997                .columns
1998                .iter()
1999                .map(std::string::String::as_str)
2000                .collect();
2001            *writer_guard = Some(client.copy_in(&self.table_name, &columns)?);
2002        }
2003
2004        // Handle headers: only first chunk should have header in the COPY stream
2005        // Each InsertChunk includes a 19-byte HyperBinary header, so we need to
2006        // strip headers from all chunks except the first one sent.
2007        let is_first = !self.header_sent.swap(true, Ordering::SeqCst);
2008
2009        let data_to_send = if is_first {
2010            // First chunk: send with header
2011            &buffer[..]
2012        } else {
2013            // Subsequent chunks: strip the 19-byte header if present
2014            if buffer.len() > hyperdb_api_core::protocol::copy::HYPER_BINARY_HEADER_SIZE
2015                && buffer.starts_with(hyperdb_api_core::protocol::copy::HYPER_BINARY_HEADER)
2016            {
2017                &buffer[hyperdb_api_core::protocol::copy::HYPER_BINARY_HEADER_SIZE..]
2018            } else {
2019                &buffer[..]
2020            }
2021        };
2022
2023        // Write the chunk directly to the socket, avoiding a full-chunk memcpy
2024        // into the connection's write buffer. flush_stream ensures the data
2025        // reaches the server before we return.
2026        if let Some(ref mut writer) = *writer_guard {
2027            writer.send_direct(data_to_send)?;
2028            writer.flush_stream()?;
2029        }
2030
2031        // Update counters (lock already released for these atomic ops)
2032        drop(writer_guard);
2033        self.total_rows
2034            .fetch_add(row_count as u64, Ordering::Relaxed);
2035        self.chunks_sent.fetch_add(1, Ordering::Relaxed);
2036
2037        debug!(
2038            target: "hyperdb_api",
2039            chunk = self.chunks_sent.load(Ordering::Relaxed),
2040            rows = row_count,
2041            bytes = data_to_send.len(),
2042            "chunk-sender"
2043        );
2044
2045        Ok(())
2046    }
2047
2048    /// Returns the total number of rows sent so far.
2049    pub fn total_rows(&self) -> u64 {
2050        self.total_rows.load(Ordering::Relaxed)
2051    }
2052
2053    /// Returns the number of chunks sent so far.
2054    pub fn chunks_sent(&self) -> usize {
2055        self.chunks_sent.load(Ordering::Relaxed)
2056    }
2057
2058    /// Finishes the COPY operation and returns the total row count.
2059    ///
2060    /// This method consumes the sender. After calling this, the COPY operation
2061    /// is complete and all data has been committed.
2062    ///
2063    /// # Errors
2064    ///
2065    /// - Returns [`Error::Internal`] with message `"ChunkSender mutex poisoned"`
2066    ///   if a sender thread panicked while holding the writer lock.
2067    /// - Returns [`Error::Server`] or [`Error::Io`] if sending the COPY
2068    ///   trailer or finishing the COPY operation fails.
2069    pub fn finish(self) -> Result<u64> {
2070        let mut writer_guard = self
2071            .writer
2072            .lock()
2073            .map_err(|_| Error::internal("ChunkSender mutex poisoned"))?;
2074
2075        // If no chunks were sent, return 0
2076        let Some(writer) = writer_guard.take() else {
2077            return Ok(0);
2078        };
2079
2080        // Write and send the COPY trailer
2081        let mut trailer_buf = BytesMut::with_capacity(2);
2082        copy::write_trailer(&mut trailer_buf);
2083
2084        // Need to get mutable access to send trailer
2085        let mut writer = writer;
2086        writer.send(&trailer_buf)?;
2087
2088        // Finish the COPY operation
2089        let rows = writer.finish()?;
2090
2091        info!(
2092            target: "hyperdb_api",
2093            rows,
2094            chunks = self.chunks_sent.load(Ordering::Relaxed),
2095            table = %self.table_name,
2096            "chunk-sender-finish"
2097        );
2098
2099        Ok(rows)
2100    }
2101}
2102
2103#[cfg(test)]
2104mod tests {
2105    use crate::table_definition::TableDefinition;
2106    use hyperdb_api_core::types::SqlType;
2107
2108    use super::InsertChunk;
2109
2110    fn create_test_table_def() -> TableDefinition {
2111        TableDefinition::new("test")
2112            .add_required_column("id", SqlType::int())
2113            .add_nullable_column("name", SqlType::text())
2114    }
2115
2116    #[test]
2117    fn test_inserter_column_validation() {
2118        // We can't fully test without a connection, but we can test validation logic
2119        let table_def = create_test_table_def();
2120        assert_eq!(table_def.column_count(), 2);
2121    }
2122
2123    #[test]
2124    fn test_insert_chunk_encoding() {
2125        let table_def = create_test_table_def();
2126        let mut chunk = InsertChunk::from_table_definition(&table_def);
2127
2128        // Add a row
2129        chunk.add_i32(42).unwrap();
2130        chunk.add_str("hello").unwrap();
2131        chunk.end_row().unwrap();
2132
2133        assert_eq!(chunk.row_count(), 1);
2134        assert!(!chunk.is_empty());
2135        assert!(!chunk.should_flush()); // Not at limit yet
2136
2137        // Add more rows
2138        for i in 0..100 {
2139            chunk.add_i32(i).unwrap();
2140            chunk.add_str(&format!("item {i}")).unwrap();
2141            chunk.end_row().unwrap();
2142        }
2143
2144        assert_eq!(chunk.row_count(), 101);
2145
2146        // Take the buffer
2147        let buffer = chunk.take().unwrap();
2148        assert!(!buffer.is_empty());
2149
2150        // Chunk should now be empty after take
2151        assert!(chunk.take().is_none());
2152    }
2153
2154    #[test]
2155    fn test_insert_chunk_null_handling() {
2156        let table_def = create_test_table_def();
2157        let mut chunk = InsertChunk::from_table_definition(&table_def);
2158
2159        // First column is NOT NULL, should fail
2160        assert!(chunk.add_null().is_err());
2161
2162        // Add the required column first
2163        chunk.add_i32(1).unwrap();
2164
2165        // Second column is nullable, should succeed
2166        chunk.add_null().unwrap();
2167        chunk.end_row().unwrap();
2168
2169        assert_eq!(chunk.row_count(), 1);
2170    }
2171
2172    #[test]
2173    fn test_insert_chunk_column_count_validation() {
2174        let table_def = create_test_table_def();
2175        let mut chunk = InsertChunk::from_table_definition(&table_def);
2176
2177        // Add only one column
2178        chunk.add_i32(1).unwrap();
2179
2180        // end_row should fail
2181        assert!(chunk.end_row().is_err());
2182
2183        // Add second column
2184        chunk.add_str("test").unwrap();
2185
2186        // Now end_row should succeed
2187        chunk.end_row().unwrap();
2188    }
2189
2190    #[test]
2191    fn test_insert_chunk_too_many_columns() {
2192        let table_def = create_test_table_def();
2193        let mut chunk = InsertChunk::from_table_definition(&table_def);
2194
2195        chunk.add_i32(1).unwrap();
2196        chunk.add_str("test").unwrap();
2197
2198        // Third column should fail
2199        assert!(chunk.add_i32(2).is_err());
2200    }
2201
2202    #[test]
2203    fn test_insert_chunk_clear() {
2204        let table_def = create_test_table_def();
2205        let mut chunk = InsertChunk::from_table_definition(&table_def);
2206
2207        chunk.add_i32(1).unwrap();
2208        chunk.add_str("test").unwrap();
2209        chunk.end_row().unwrap();
2210
2211        assert_eq!(chunk.row_count(), 1);
2212
2213        chunk.clear();
2214
2215        assert_eq!(chunk.row_count(), 0);
2216        assert!(chunk.is_empty());
2217    }
2218}