Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions DuckDB.NET.Benchmarks/AppenderBenchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,21 @@ public void AppendRowsWithAppendRow()
});
}
}

[Benchmark]
public void AppendRowsWithScopedWriter()
{
using var appender = connection.CreateAppender("bench");

for (var i = 0; i < RowCount; i++)
{
appender.AppendRowScoped(i, static (ref DuckDBAppenderRowWriter writer, int value) =>
{
writer.AppendValue(value);
writer.AppendValue((long)value);
writer.AppendValue((double)value);
writer.AppendValue(value % 2 == 0);
});
}
}
}
97 changes: 76 additions & 21 deletions DuckDB.NET.Data/DuckDBAppender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ namespace DuckDB.NET.Data;
/// </summary>
/// <remarks>
/// Instances are not thread-safe. Do not call other methods on the same appender from an
/// <see cref="AppendRow{TState}(TState, Action{IDuckDBAppenderRow, TState})"/> callback.
/// <see cref="AppendRow{TState}(TState, Action{IDuckDBAppenderRow, TState})"/> or
/// <see cref="AppendRowScoped{TState}(TState, DuckDBAppenderRowWriterAction{TState})"/> callback.
/// </remarks>
public class DuckDBAppender : IDisposable
{
Expand Down Expand Up @@ -109,21 +110,46 @@ public void AppendRow<TState>(TState state, Action<IDuckDBAppenderRow, TState> w
}
catch (Exception appendException)
{
if (row is not null)
{
try
{
FinalizeFailedAppendRow(row);
}
catch (Exception finalizationException)
{
throw new AggregateException(
"Appending the row failed and the previously completed rows could not be finalized",
appendException,
finalizationException);
}
}
FinalizeFailedAppendRowOrThrow(row, appendException);
throw;
}
finally
{
isAppendingRow = false;
}
}

/// <summary>
/// Appends a complete row through a stack-only writer that cannot escape the callback.
/// </summary>
/// <typeparam name="TState">The type of value used to populate the row.</typeparam>
/// <param name="state">The value used to populate the row.</param>
/// <param name="writeRow">A callback that appends every column value. This method validates
/// and completes the row after the callback returns.</param>
/// <remarks>
/// The callback must not call other methods on this appender. If the callback or automatic
/// row completion fails, all previously completed rows are flushed, the failed row is
/// discarded, and the appender cannot be reused. Use a static or cached callback for an
/// allocation-free per-row path; a capturing callback can allocate.
/// </remarks>
public void AppendRowScoped<TState>(TState state, DuckDBAppenderRowWriterAction<TState> writeRow)
{
ArgumentNullException.ThrowIfNull(writeRow);
EnsureUsable();

DuckDBAppenderRow? row = null;
isAppendingRow = true;

try
{
row = CreateReusableRow();
var writer = new DuckDBAppenderRowWriter(row);
writeRow(ref writer, state);
row.EndRow();
}
catch (Exception appendException)
{
FinalizeFailedAppendRowOrThrow(row, appendException);
throw;
}
finally
Expand Down Expand Up @@ -158,9 +184,12 @@ private ulong PrepareRow()
{
AppendDataChunk();

InitVectorWriters();

// AppendDataChunk resets the chunk. Update the managed count before recreating the
// writers so a writer-initialization failure cannot make CloseCore append the already
// completed chunk a second time.
rowCount = 0;

InitVectorWriters();
}

rowCount++;
Expand Down Expand Up @@ -262,16 +291,42 @@ private void AppendDataChunk()
NativeMethods.DataChunks.DuckDBDataChunkReset(dataChunk);
}

private void FinalizeFailedAppendRow(DuckDBAppenderRow row)
private void FinalizeFailedAppendRow(DuckDBAppenderRow? row)
{
// The row index is also the number of completed rows before the failed row in this chunk.
rowCount = row.ChunkRowIndex;
row.Invalidate();
isFaulted = true;

if (row is null)
{
// Row preparation failed while flushing or recreating the current chunk. Discard any
// uncommitted managed rows by closing the chunk at size zero so the appender cannot be
// reused in a partially transitioned state.
rowCount = 0;
}
else
{
// The row index is also the number of completed rows before the failed row in this chunk.
rowCount = row.ChunkRowIndex;
row.Invalidate();
}

CloseCore();
}

private void FinalizeFailedAppendRowOrThrow(DuckDBAppenderRow? row, Exception appendException)
{
try
{
FinalizeFailedAppendRow(row);
}
catch (Exception finalizationException)
{
throw new AggregateException(
"Appending the row failed and the previously completed rows could not be finalized",
appendException,
finalizationException);
}
}

private void EnsureNotAppendingRow()
{
if (isAppendingRow)
Expand Down
111 changes: 111 additions & 0 deletions DuckDB.NET.Data/DuckDBAppenderRowWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using System.Runtime.CompilerServices;

namespace DuckDB.NET.Data;

/// <summary>
/// A stack-only writer for a single appender row.
/// </summary>
/// <remarks>
/// Instances are valid only for the duration of a
/// <see cref="DuckDBAppender.AppendRowScoped{TState}(TState, DuckDBAppenderRowWriterAction{TState})"/>
/// callback. The stack-only type cannot be boxed, captured, or stored on the managed heap.
/// Row completion is performed by the appender after the callback returns.
/// </remarks>
public ref struct DuckDBAppenderRowWriter
{
private readonly DuckDBAppenderRow row;

internal DuckDBAppenderRowWriter(DuckDBAppenderRow row)
{
this.row = row;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendNullValue() => row.AppendNullValue();

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(bool? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(byte[]? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(Span<byte> value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(string? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(decimal? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(Guid? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(BigInteger? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(sbyte? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(short? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(int? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(long? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(byte? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(ushort? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(uint? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(ulong? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue<TEnum>(TEnum? value) where TEnum : Enum => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(float? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(double? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(DateOnly? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(TimeOnly? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(DuckDBDateOnly? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(DuckDBTimeOnly? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(DateTime? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(DateTimeOffset? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue(TimeSpan? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendValue<T>(IEnumerable<T>? value) => row.AppendValue(value);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendDefault() => row.AppendDefault();
}

/// <summary>
/// Writes one complete row through a stack-only <see cref="DuckDBAppenderRowWriter"/>.
/// </summary>
public delegate void DuckDBAppenderRowWriterAction<TState>(ref DuckDBAppenderRowWriter row, TState state);
16 changes: 12 additions & 4 deletions DuckDB.NET.Data/Extensions/DateTimeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,22 @@ public static DuckDBTimeTzStruct ToTimeTzStruct(this DateTimeOffset value)

public static DuckDBTimestampStruct ToTimestampStruct(this DateTimeOffset value)
{
var timestamp = DuckDBTimestamp.FromDateTime(value.UtcDateTime).ToDuckDBTimestampStruct();

return timestamp;
return value.UtcDateTime.ToTimestampStruct(DuckDBType.Timestamp);
}

public static DuckDBTimestampStruct ToTimestampStruct(this DateTime value, DuckDBType duckDBType)
{
var timestamp = DuckDBTimestamp.FromDateTime(value).ToDuckDBTimestampStruct();
var ticksSinceEpoch = value.Ticks - DateTime.UnixEpoch.Ticks;
var microseconds = ticksSinceEpoch / TicksPerMicrosecond;

// duckdb_to_timestamp truncates the time-of-day to microseconds after resolving the date,
// which is floor division for pre-epoch values with sub-microsecond ticks.
if (ticksSinceEpoch < 0 && ticksSinceEpoch % TicksPerMicrosecond != 0)
{
microseconds--;
}

var timestamp = new DuckDBTimestampStruct { Micros = microseconds };

if (duckDBType == DuckDBType.TimestampNs)
{
Expand Down
Loading
Loading