@@ -269,34 +269,63 @@ func (i *Ingester) writeRows(ctx context.Context, fqTable string, rows []map[str
269269 continue
270270 }
271271 cols := sortedMapKeys (bucket [0 ])
272- placeholders := make ([]string , len (cols ))
273- for j := range placeholders {
274- placeholders [j ] = "?"
272+ n , err := i .insertBucket (ctx , fqTable , cols , bucket )
273+ inserted += n
274+ if err != nil {
275+ return inserted , err
276+ }
277+ }
278+ return inserted , nil
279+ }
280+
281+ // lpInsertChunkRows bounds how many rows go into a single multi-row INSERT
282+ // so a large batch doesn't blow past DuckDB's bound-parameter / statement
283+ // size limits. Chosen well below any practical limit while still collapsing
284+ // thousands of per-row commits into a handful of statements.
285+ const lpInsertChunkRows = 500
286+
287+ // insertBucket writes a homogeneous set of rows (same column subset) using
288+ // chunked multi-row `INSERT ... VALUES (...),(...),...` statements instead of
289+ // one Exec per row.
290+ //
291+ // Each per-row Exec used to be its own DuckLake transaction — a catalog
292+ // snapshot plus a tiny Parquet (or inlined) write per row. Under sustained
293+ // Line Protocol traffic that micro-commit storm bloats the catalog and stalls
294+ // ingest (the same pattern fixed for OTLP/Python in the sibling ingest
295+ // service). Bulk inserts cut the transaction/snapshot count by up to
296+ // lpInsertChunkRows×.
297+ func (i * Ingester ) insertBucket (ctx context.Context , fqTable string , cols []string , bucket []map [string ]interface {}) (int , error ) {
298+ colList := strings .Join (cols , ", " )
299+ // "(?, ?, ... ?)" for one row.
300+ rowPlaceholder := "(" + strings .TrimSuffix (strings .Repeat ("?, " , len (cols )), ", " ) + ")"
301+
302+ inserted := 0
303+ for start := 0 ; start < len (bucket ); start += lpInsertChunkRows {
304+ end := start + lpInsertChunkRows
305+ if end > len (bucket ) {
306+ end = len (bucket )
307+ }
308+ chunk := bucket [start :end ]
309+
310+ placeholders := make ([]string , len (chunk ))
311+ args := make ([]interface {}, 0 , len (chunk )* len (cols ))
312+ for k , r := range chunk {
313+ placeholders [k ] = rowPlaceholder
314+ for _ , c := range cols {
315+ args = append (args , r [c ])
316+ }
275317 }
276318 stmtSQL := fmt .Sprintf (
277- "INSERT INTO %s (%s) VALUES (%s) " ,
319+ "INSERT INTO %s (%s) VALUES %s " ,
278320 fqTable ,
279- strings . Join ( cols , ", " ) ,
321+ colList ,
280322 strings .Join (placeholders , ", " ),
281323 )
282- stmt , err := i .db .PrepareContext (ctx , stmtSQL )
283- if err != nil {
284- metrics .RecordLineProtoWriteError ("prepare" )
285- return inserted , fmt .Errorf ("prepare: %w" , err )
286- }
287- for _ , r := range bucket {
288- vals := make ([]interface {}, len (cols ))
289- for j , c := range cols {
290- vals [j ] = r [c ]
291- }
292- if _ , err := stmt .ExecContext (ctx , vals ... ); err != nil {
293- _ = stmt .Close ()
294- metrics .RecordLineProtoWriteError ("insert" )
295- return inserted , fmt .Errorf ("exec: %w" , err )
296- }
297- inserted ++
324+ if _ , err := i .db .ExecContext (ctx , stmtSQL , args ... ); err != nil {
325+ metrics .RecordLineProtoWriteError ("insert" )
326+ return inserted , fmt .Errorf ("bulk insert: %w" , err )
298327 }
299- _ = stmt . Close ( )
328+ inserted += len ( chunk )
300329 }
301330 return inserted , nil
302331}
0 commit comments