Skip to content
Merged
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
41 changes: 41 additions & 0 deletions src/BLite.Core/BLiteValidationException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;

namespace BLite.Core;

/// <summary>
/// Thrown by a collection write method when it rejects its input before writing anything:
/// a duplicate primary key, a duplicate value for a unique index, a vector whose length does not
/// match its vector index, or a document or index key that is too large to be stored.
/// </summary>
/// <remarks>
/// <para>
/// This is the only exception after which a write that ran in a caller-owned transaction leaves
/// that transaction usable. A single-document write (insert, update, upsert) that throws it wrote
/// nothing, so the caller can catch it, carry on with the same transaction and commit. A bulk write
/// that throws it on its first document wrote nothing either; one that throws it after earlier
/// documents of the batch were written rolls the transaction back like any other failure.
/// </para>
/// <para>
/// Any other exception thrown while a write method is modifying pages rolls the caller's
/// transaction back (<see cref="Transactions.ITransaction.State"/> becomes
/// <see cref="Transactions.TransactionState.Aborted"/>), because the failed write may already have
/// updated the data page and some of the indexes but not the others, and committing that would
/// leave the indexes out of step with the data.
/// </para>
/// <para>
/// Derives from <see cref="InvalidOperationException"/>, the type these errors had before, except
/// that a vector dimension mismatch used to surface as an <see cref="ArgumentException"/>.
/// </para>
/// </remarks>
public class BLiteValidationException : InvalidOperationException
{
/// <summary>Creates the exception with a message describing the rejected input.</summary>
public BLiteValidationException(string message) : base(message)
{
}

/// <summary>Creates the exception with a message and the error that caused it.</summary>
public BLiteValidationException(string message, Exception innerException) : base(message, innerException)
{
}
}
156 changes: 129 additions & 27 deletions src/BLite.Core/Collections/DocumentCollection.cs

Large diffs are not rendered by default.

63 changes: 63 additions & 0 deletions src/BLite.Core/Collections/IDocumentCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,25 @@ public interface IDocumentCollection<TId, T> where T : class
// ── Insert ────────────────────────────────────────────────────────────────

ValueTask<TId> InsertAsync(T entity, CancellationToken ct = default);
/// <remarks>
/// With a caller-owned <paramref name="transaction"/>: if the call throws
/// <see cref="BLiteValidationException"/> (duplicate key, vector dimension mismatch, document or
/// key too large), it wrote nothing and the transaction stays active, so the caller can catch it,
/// carry on and commit. Any other failure while writing rolls the transaction back
/// (<see cref="ITransaction.State"/> becomes <see cref="TransactionState.Aborted"/>), so that a
/// half-applied write, with the data and its indexes out of step, can never be committed.
/// </remarks>
ValueTask<TId> InsertAsync(T entity, ITransaction? transaction, CancellationToken ct = default);

ValueTask<List<TId>> InsertBulkAsync(IEnumerable<T> entities, CancellationToken ct = default);
/// <remarks>
/// With a caller-owned <paramref name="transaction"/> the call is all or nothing: if the first
/// document is rejected with <see cref="BLiteValidationException"/> (duplicate key, vector
/// dimension mismatch, document or key too large), nothing was written and the transaction stays
/// active. Any other failure, including a rejected document after earlier documents of the batch
/// were written, rolls the transaction back (<see cref="ITransaction.State"/> becomes
/// <see cref="TransactionState.Aborted"/>).
/// </remarks>
ValueTask<List<TId>> InsertBulkAsync(IEnumerable<T> entities, ITransaction? transaction, CancellationToken ct = default);

// ── Read ──────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -72,9 +88,25 @@ public interface IDocumentCollection<TId, T> where T : class
// ── Update ────────────────────────────────────────────────────────────────

ValueTask<bool> UpdateAsync(T entity, CancellationToken ct = default);
/// <remarks>
/// With a caller-owned <paramref name="transaction"/>: if the call throws
/// <see cref="BLiteValidationException"/> (duplicate key, vector dimension mismatch, document or
/// key too large), it wrote nothing and the transaction stays active, so the caller can catch it,
/// carry on and commit. Any other failure while writing rolls the transaction back
/// (<see cref="ITransaction.State"/> becomes <see cref="TransactionState.Aborted"/>), so that a
/// half-applied write, with the data and its indexes out of step, can never be committed.
/// </remarks>
ValueTask<bool> UpdateAsync(T entity, ITransaction? transaction, CancellationToken ct = default);

ValueTask<int> UpdateBulkAsync(IEnumerable<T> entities, CancellationToken ct = default);
/// <remarks>
/// With a caller-owned <paramref name="transaction"/> the call is all or nothing: if the first
/// document is rejected with <see cref="BLiteValidationException"/> (duplicate key, vector
/// dimension mismatch, document or key too large), nothing was written and the transaction stays
/// active. Any other failure, including a rejected document after earlier documents of the batch
/// were written, rolls the transaction back (<see cref="ITransaction.State"/> becomes
/// <see cref="TransactionState.Aborted"/>).
/// </remarks>
ValueTask<int> UpdateBulkAsync(IEnumerable<T> entities, ITransaction? transaction, CancellationToken ct = default);

// ── Upsert ────────────────────────────────────────────────────────────────
Expand All @@ -83,17 +115,43 @@ public interface IDocumentCollection<TId, T> where T : class
// per document — not a Find followed by a separate Insert/Update call.

ValueTask<UpsertResult<TId>> UpsertAsync(T entity, CancellationToken ct = default);
/// <remarks>
/// With a caller-owned <paramref name="transaction"/>: if the call throws
/// <see cref="BLiteValidationException"/> (duplicate key, vector dimension mismatch, document or
/// key too large), it wrote nothing and the transaction stays active, so the caller can catch it,
/// carry on and commit. Any other failure while writing rolls the transaction back
/// (<see cref="ITransaction.State"/> becomes <see cref="TransactionState.Aborted"/>), so that a
/// half-applied write, with the data and its indexes out of step, can never be committed.
/// </remarks>
ValueTask<UpsertResult<TId>> UpsertAsync(T entity, ITransaction? transaction, CancellationToken ct = default);

ValueTask<List<UpsertResult<TId>>> UpsertBulkAsync(IEnumerable<T> entities, CancellationToken ct = default);
/// <remarks>
/// With a caller-owned <paramref name="transaction"/> the call is all or nothing: if the first
/// document is rejected with <see cref="BLiteValidationException"/> (duplicate key, vector
/// dimension mismatch, document or key too large), nothing was written and the transaction stays
/// active. Any other failure, including a rejected document after earlier documents of the batch
/// were written, rolls the transaction back (<see cref="ITransaction.State"/> becomes
/// <see cref="TransactionState.Aborted"/>).
/// </remarks>
ValueTask<List<UpsertResult<TId>>> UpsertBulkAsync(IEnumerable<T> entities, ITransaction? transaction, CancellationToken ct = default);

// ── Delete ────────────────────────────────────────────────────────────────

ValueTask<bool> DeleteAsync(TId id, CancellationToken ct = default);
/// <remarks>
/// With a caller-owned <paramref name="transaction"/>, a failure while writing rolls the
/// transaction back (<see cref="ITransaction.State"/> becomes <see cref="TransactionState.Aborted"/>),
/// so that a half-applied delete can never be committed.
/// </remarks>
ValueTask<bool> DeleteAsync(TId id, ITransaction? transaction, CancellationToken ct = default);

ValueTask<int> DeleteBulkAsync(IEnumerable<TId> ids, CancellationToken ct = default);
/// <remarks>
/// With a caller-owned <paramref name="transaction"/>, a failure while writing rolls the
/// transaction back (<see cref="ITransaction.State"/> becomes <see cref="TransactionState.Aborted"/>),
/// so that a half-applied delete can never be committed.
/// </remarks>
ValueTask<int> DeleteBulkAsync(IEnumerable<TId> ids, ITransaction? transaction, CancellationToken ct = default);

// ── Index management ──────────────────────────────────────────────────────
Expand Down Expand Up @@ -185,6 +243,11 @@ IAsyncEnumerable<T> ParallelScanAsync(
Task<int> TruncateAsync(CancellationToken ct = default);

/// <inheritdoc cref="TruncateAsync(CancellationToken)"/>
/// <remarks>
/// With a caller-owned <paramref name="transaction"/>, a failure while writing rolls the
/// transaction back (<see cref="ITransaction.State"/> becomes <see cref="TransactionState.Aborted"/>),
/// so that a half-applied delete can never be committed.
/// </remarks>
Task<int> TruncateAsync(ITransaction? transaction, CancellationToken ct = default);

// ── TimeSeries (local engine only; remote throws NotSupportedException) ───
Expand Down
Loading
Loading