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
16 changes: 16 additions & 0 deletions src/Marten/Events/EventDocumentStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,22 @@ public IStorageOperation OverwriteProjected(IEvent document, string tenant)
throw new NotSupportedException();
}

// #4667 — events aren't projected through the document write path.
public IStorageOperation UpsertProjected(IEvent document, string tenant)
{
throw new NotSupportedException();
}

public IStorageOperation InsertProjected(IEvent document, string tenant)
{
throw new NotSupportedException();
}

public IStorageOperation UpdateProjected(IEvent document, string tenant)
{
throw new NotSupportedException();
}

public abstract IStorageOperation AppendEvent(EventGraph events, IMartenSession session, StreamAction stream,
IEvent e);

Expand Down
16 changes: 16 additions & 0 deletions src/Marten/Events/EventMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,22 @@ IStorageOperation IDocumentStorage<T>.OverwriteProjected(T document, string tena
throw new NotSupportedException();
}

// #4667 — events aren't projected through the document write path.
IStorageOperation IDocumentStorage<T>.UpsertProjected(T document, string tenant)
{
throw new NotSupportedException();
}

IStorageOperation IDocumentStorage<T>.InsertProjected(T document, string tenant)
{
throw new NotSupportedException();
}

IStorageOperation IDocumentStorage<T>.UpdateProjected(T document, string tenant)
{
throw new NotSupportedException();
}

public IDeletion DeleteForDocument(T document, string tenant)
{
throw new NotSupportedException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ internal sealed class NumericClosedShapeInsertOperation<TDoc, TId>: ClosedShapeI
where TDoc : notnull
where TId : notnull
{
private readonly Dictionary<TId, long> _revisions;
private readonly Dictionary<TId, long>? _revisions;

public NumericClosedShapeInsertOperation(
TDoc document,
TId id,
string tenantId,
DocumentStorageDescriptor<TDoc, TId> descriptor,
Dictionary<TId, long> revisions)
Dictionary<TId, long>? revisions)
: base(document, id, tenantId, descriptor)
{
_revisions = revisions;
Expand Down Expand Up @@ -67,7 +67,13 @@ public override async Task PostprocessAsync(DbDataReader reader, IList<Exception
}

var newRevision = await reader.GetFieldValueAsync<long>(0, token).ConfigureAwait(false);
_revisions[_id] = newRevision;
// #4667 — null tracker (the InsertProjected path) skips the tracker
// write. RevisionBinder still applies so the document's revision
// field is fresh.
if (_revisions is not null)
{
_revisions[_id] = newRevision;
}
_descriptor.RevisionBinder!.ApplyRevisionTo(_document, newRevision);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ internal sealed class NumericClosedShapeUpdateOperation<TDoc, TId>: ClosedShapeU
where TDoc : notnull
where TId : notnull
{
private readonly Dictionary<TId, long> _revisions;
private readonly Dictionary<TId, long>? _revisions;

public NumericClosedShapeUpdateOperation(
TDoc document,
TId id,
string tenantId,
DocumentStorageDescriptor<TDoc, TId> descriptor,
Dictionary<TId, long> revisions)
Dictionary<TId, long>? revisions)
: base(document, id, tenantId, descriptor)
{
_revisions = revisions;
Expand Down Expand Up @@ -66,7 +66,13 @@ public override async Task PostprocessAsync(DbDataReader reader, IList<Exception
}

var newRevision = await reader.GetFieldValueAsync<long>(0, token).ConfigureAwait(false);
_revisions[_id] = newRevision;
// #4667 — null tracker (the UpdateProjected path) skips the tracker
// write. RevisionBinder still applies so the document's revision
// field is fresh.
if (_revisions is not null)
{
_revisions[_id] = newRevision;
}
_descriptor.RevisionBinder!.ApplyRevisionTo(_document, newRevision);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ internal sealed class NumericClosedShapeUpsertOperation<TDoc, TId>: ClosedShapeU
where TDoc : notnull
where TId : notnull
{
private readonly Dictionary<TId, long> _revisions;
private readonly Dictionary<TId, long>? _revisions;

public NumericClosedShapeUpsertOperation(
TDoc document,
TId id,
string tenantId,
DocumentStorageDescriptor<TDoc, TId> descriptor,
OperationRole role,
Dictionary<TId, long> revisions)
Dictionary<TId, long>? revisions)
: base(document, id, tenantId, descriptor, role)
{
_revisions = revisions;
Expand Down Expand Up @@ -68,7 +68,13 @@ public override async Task PostprocessAsync(DbDataReader reader, IList<Exception
}

var newRevision = await reader.GetFieldValueAsync<long>(0, token).ConfigureAwait(false);
_revisions[_id] = newRevision;
// #4667 — null tracker (the UpsertProjected path) skips the tracker
// write. RevisionBinder still applies so the document's revision
// field is fresh.
if (_revisions is not null)
{
_revisions[_id] = newRevision;
}
_descriptor.RevisionBinder!.ApplyRevisionTo(_document, newRevision);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ public override IStorageOperation Overwrite(TDoc document, IMartenSession sessio
public override IStorageOperation OverwriteProjected(TDoc document, string tenant)
=> new NumericClosedShapeOverwriteOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

// #4667 — null revision tracker; see Lightweight peer for semantics.
public override IStorageOperation UpsertProjected(TDoc document, string tenant)
=> new NumericClosedShapeUpsertOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, OperationRole.Upsert, null);

public override IStorageOperation InsertProjected(TDoc document, string tenant)
=> new NumericClosedShapeInsertOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

public override IStorageOperation UpdateProjected(TDoc document, string tenant)
=> new NumericClosedShapeUpdateOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

public override ISelector BuildSelector(IMartenSession session)
=> _descriptor.HierarchyMapping is not null
? new HierarchicalNumericClosedShapeDirtyTrackingSelector<TDoc, TId>(session, _descriptor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ public override IStorageOperation Overwrite(TDoc document, IMartenSession sessio
public override IStorageOperation OverwriteProjected(TDoc document, string tenant)
=> new NumericClosedShapeOverwriteOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

// #4667 — null revision tracker; see Lightweight peer for semantics.
public override IStorageOperation UpsertProjected(TDoc document, string tenant)
=> new NumericClosedShapeUpsertOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, OperationRole.Upsert, null);

public override IStorageOperation InsertProjected(TDoc document, string tenant)
=> new NumericClosedShapeInsertOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

public override IStorageOperation UpdateProjected(TDoc document, string tenant)
=> new NumericClosedShapeUpdateOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

public override ISelector BuildSelector(IMartenSession session)
=> _descriptor.HierarchyMapping is not null
? new HierarchicalNumericClosedShapeIdentityMapSelector<TDoc, TId>(session, _descriptor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@ public override IStorageOperation OverwriteProjected(TDoc document, string tenan
// the session's numeric-revision map for the projected doc.
=> new NumericClosedShapeOverwriteOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

// #4667 — null revision tracker. Numeric ops bind the IRevisionedOperation
// `Revision` property (default 0) in ConfigureCommand, so the WHERE guard
// `? = 0 OR table.mt_version < ?` always passes when the caller leaves
// Revision at the default.
public override IStorageOperation UpsertProjected(TDoc document, string tenant)
=> new NumericClosedShapeUpsertOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, OperationRole.Upsert, null);

public override IStorageOperation InsertProjected(TDoc document, string tenant)
=> new NumericClosedShapeInsertOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

public override IStorageOperation UpdateProjected(TDoc document, string tenant)
=> new NumericClosedShapeUpdateOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

public override ISelector BuildSelector(IMartenSession session)
=> _descriptor.HierarchyMapping is not null
? new HierarchicalNumericClosedShapeLightweightSelector<TDoc, TId>(session, _descriptor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ internal sealed class OptimisticClosedShapeInsertOperation<TDoc, TId>: ClosedSha
where TDoc : notnull
where TId : notnull
{
private readonly Dictionary<TId, Guid> _versions;
private readonly Dictionary<TId, Guid>? _versions;
private readonly Guid _newVersion;

public OptimisticClosedShapeInsertOperation(
TDoc document,
TId id,
string tenantId,
DocumentStorageDescriptor<TDoc, TId> descriptor,
Dictionary<TId, Guid> versions)
Dictionary<TId, Guid>? versions)
: base(document, id, tenantId, descriptor)
{
_versions = versions;
Expand Down Expand Up @@ -69,6 +69,12 @@ public override async Task PostprocessAsync(DbDataReader reader, IList<Exception
return;
}

_versions[_id] = _newVersion;
// #4667 — null tracker (the InsertProjected path) just skips the
// tracker write. The fresh version is still applied to the document
// via VersionBinder during command configuration.
if (_versions is not null)
{
_versions[_id] = _newVersion;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ internal sealed class OptimisticClosedShapeUpdateOperation<TDoc, TId>: ClosedSha
where TDoc : notnull
where TId : notnull
{
private readonly Dictionary<TId, Guid> _versions;
private readonly Dictionary<TId, Guid>? _versions;
private readonly Guid _newVersion;

public OptimisticClosedShapeUpdateOperation(
TDoc document,
TId id,
string tenantId,
DocumentStorageDescriptor<TDoc, TId> descriptor,
Dictionary<TId, Guid> versions)
Dictionary<TId, Guid>? versions)
: base(document, id, tenantId, descriptor)
{
_versions = versions;
Expand All @@ -46,8 +46,12 @@ public override void ConfigureCommand(ICommandBuilder builder, IMartenSession se
var parameters = builder.AppendWithParameters(_descriptor.UpdateSql, '?');
var slot = BindPreConcurrencyParameters(parameters, session);

// Trailing WHERE mt_version = ? guard.
if (_versions.TryGetValue(_id, out var expected))
// Trailing WHERE mt_version = ? guard. #4667 — null tracker (the
// UpdateProjected path) treats expected version as DBNull, which the
// SQL WHERE never matches. Callers that go through UpdateProjected
// should also set IgnoreConcurrencyViolation = true to suppress the
// resulting "no row" exception.
if (_versions is not null && _versions.TryGetValue(_id, out var expected))
{
parameters[slot].Value = expected;
}
Expand All @@ -69,7 +73,13 @@ public override async Task PostprocessAsync(DbDataReader reader, IList<Exception
return;
}

_versions[_id] = _newVersion;
// #4667 — null tracker (the UpdateProjected path) just skips the
// tracker write. The fresh version is still applied to the document
// via VersionBinder during command configuration.
if (_versions is not null)
{
_versions[_id] = _newVersion;
}
}

protected override int BindClientSideBinder(NpgsqlParameter[] parameters, int slot, IDocumentMetadataBinder<TDoc> binder, IMartenSession session)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal sealed class OptimisticClosedShapeUpsertOperation<TDoc, TId>: ClosedSha
where TDoc : notnull
where TId : notnull
{
private readonly Dictionary<TId, Guid> _versions;
private readonly Dictionary<TId, Guid>? _versions;
private readonly Guid _newVersion;

public OptimisticClosedShapeUpsertOperation(
Expand All @@ -36,7 +36,7 @@ public OptimisticClosedShapeUpsertOperation(
string tenantId,
DocumentStorageDescriptor<TDoc, TId> descriptor,
OperationRole role,
Dictionary<TId, Guid> versions)
Dictionary<TId, Guid>? versions)
: base(document, id, tenantId, descriptor, role)
{
_versions = versions;
Expand All @@ -48,8 +48,12 @@ public override void ConfigureCommand(ICommandBuilder builder, IMartenSession se
var parameters = builder.AppendWithParameters(_descriptor.UpsertSql, '?');
var slot = BindPreOnConflictParameters(parameters, session);

// Trailing WHERE table.mt_version = ? guard.
if (_versions.TryGetValue(_id, out var expected))
// Trailing WHERE table.mt_version = ? guard. #4667 — null tracker
// (the UpsertProjected path) treats expected version as DBNull. On
// the ON CONFLICT branch the WHERE will never match, so existing
// rows in Optimistic mode are left untouched. The INSERT branch
// still fires for new rows.
if (_versions is not null && _versions.TryGetValue(_id, out var expected))
{
parameters[slot].Value = expected;
}
Expand All @@ -71,7 +75,13 @@ public override async Task PostprocessAsync(DbDataReader reader, IList<Exception
return;
}

_versions[_id] = _newVersion;
// #4667 — null tracker (the UpsertProjected path) just skips the
// tracker write. The fresh version is still applied to the document
// via VersionBinder during command configuration.
if (_versions is not null)
{
_versions[_id] = _newVersion;
}
}

protected override int BindClientSideBinder(NpgsqlParameter[] parameters, int slot, IDocumentMetadataBinder<TDoc> binder, IMartenSession session)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ public override IStorageOperation Overwrite(TDoc document, IMartenSession sessio
public override IStorageOperation OverwriteProjected(TDoc document, string tenant)
=> new OptimisticClosedShapeOverwriteOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

// #4667 — null version tracker; see Lightweight peer for semantics.
public override IStorageOperation UpsertProjected(TDoc document, string tenant)
=> new OptimisticClosedShapeUpsertOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, OperationRole.Upsert, null);

public override IStorageOperation InsertProjected(TDoc document, string tenant)
=> new OptimisticClosedShapeInsertOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

public override IStorageOperation UpdateProjected(TDoc document, string tenant)
=> new OptimisticClosedShapeUpdateOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

public override ISelector BuildSelector(IMartenSession session)
=> _descriptor.HierarchyMapping is not null
? new HierarchicalOptimisticClosedShapeDirtyTrackingSelector<TDoc, TId>(session, _descriptor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ public override IStorageOperation Overwrite(TDoc document, IMartenSession sessio
public override IStorageOperation OverwriteProjected(TDoc document, string tenant)
=> new OptimisticClosedShapeOverwriteOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

// #4667 — null version tracker; see Lightweight peer for semantics.
public override IStorageOperation UpsertProjected(TDoc document, string tenant)
=> new OptimisticClosedShapeUpsertOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, OperationRole.Upsert, null);

public override IStorageOperation InsertProjected(TDoc document, string tenant)
=> new OptimisticClosedShapeInsertOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

public override IStorageOperation UpdateProjected(TDoc document, string tenant)
=> new OptimisticClosedShapeUpdateOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

public override ISelector BuildSelector(IMartenSession session)
=> _descriptor.HierarchyMapping is not null
? new HierarchicalOptimisticClosedShapeIdentityMapSelector<TDoc, TId>(session, _descriptor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ public override IStorageOperation OverwriteProjected(TDoc document, string tenan
// the session's optimistic-version map for the projected doc.
=> new OptimisticClosedShapeOverwriteOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

// #4667 — projection write paths pass null tracker (no session-shared
// dict access). Optimistic Upsert/Update with null tracker bind DBNull
// for the WHERE-guard; existing rows are left untouched on the ON CONFLICT
// branch. Callers in the projection runtime set IgnoreConcurrencyViolation
// = true (see ProjectionStorage.StoreProjection / Store flows) to suppress
// the resulting "no row" exception.
public override IStorageOperation UpsertProjected(TDoc document, string tenant)
=> new OptimisticClosedShapeUpsertOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, OperationRole.Upsert, null);

public override IStorageOperation InsertProjected(TDoc document, string tenant)
=> new OptimisticClosedShapeInsertOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

public override IStorageOperation UpdateProjected(TDoc document, string tenant)
=> new OptimisticClosedShapeUpdateOperation<TDoc, TId>(document, Identity(document), tenant, _descriptor, null);

public override ISelector BuildSelector(IMartenSession session)
=> _descriptor.HierarchyMapping is not null
? new HierarchicalOptimisticClosedShapeLightweightSelector<TDoc, TId>(session, _descriptor)
Expand Down
10 changes: 10 additions & 0 deletions src/Marten/Internal/ClosedShape/QueryOnlyClosedShapeStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ public override IStorageOperation Overwrite(TDoc document, IMartenSession sessio
public override IStorageOperation OverwriteProjected(TDoc document, string tenant)
=> throw new NotSupportedException("QueryOnly storage doesn't support OverwriteProjected.");

// #4667 — projection write paths aren't reachable from query sessions.
public override IStorageOperation UpsertProjected(TDoc document, string tenant)
=> throw new NotSupportedException("QueryOnly storage doesn't support UpsertProjected.");

public override IStorageOperation InsertProjected(TDoc document, string tenant)
=> throw new NotSupportedException("QueryOnly storage doesn't support InsertProjected.");

public override IStorageOperation UpdateProjected(TDoc document, string tenant)
=> throw new NotSupportedException("QueryOnly storage doesn't support UpdateProjected.");

public override ISelector BuildSelector(IMartenSession session)
// #4659 Phase 2: pick the Flat / Hierarchical selector ONCE per
// query — neither selector class branches on HierarchyMapping per
Expand Down
Loading
Loading