From 647bf8092cb41e3ff63f1dca1da89d81ce17f9ef Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Fri, 19 Jun 2015 14:43:19 +0100 Subject: [PATCH 01/34] First stab at Postgres implementation --- default.ps1 | 7 + .../Cedar.EventStore.Postgres.Tests.csproj | 99 ++++ ...ventStore.Postgres.Tests.v2.ncrunchproject | Bin 0 -> 2838 bytes .../PostgresEventStoreFixture.cs | 23 + .../PostgresEventStoreTests.cs | 10 + .../Properties/AssemblyInfo.cs | 36 ++ .../packages.config | 8 + .../Cedar.EventStore.Postgres.csproj | 94 ++++ .../Cedar.EventStore.Postgres.nuspec | 29 ++ ...edar.EventStore.Postgres.v2.ncrunchproject | Bin 0 -> 2964 bytes .../CheckpointExtensions.cs | 14 + .../InterlockedBoolean.cs | 72 +++ .../InterlockedBooleanExtensions.cs | 10 + .../PostgresEventStore.cs | 481 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 36 ++ .../SqlScripts/CreateStream.sql | 20 + .../SqlScripts/DeleteStreamAnyVersion.sql | 15 + .../DeleteStreamExpectedVersion.sql | 37 ++ .../SqlScripts/Dev.sql | 306 +++++++++++ .../SqlScripts/DropAll.sql | 3 + .../SqlScripts/GetVersion.sql | 3 + .../SqlScripts/InitializeStore.sql | 197 +++++++ .../SqlScripts/ReadAllBackward.sql | 15 + .../SqlScripts/ReadAllForward.sql | 15 + .../SqlScripts/ReadStreamBackward.sql | 31 ++ .../SqlScripts/ReadStreamForward.sql | 32 ++ .../SqlScripts/Scripts.cs | 78 +++ src/Cedar.EventStore.Postgres/packages.config | 5 + src/Cedar.EventStore.sln | 12 + 29 files changed, 1688 insertions(+) create mode 100644 src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.csproj create mode 100644 src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.v2.ncrunchproject create mode 100644 src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreFixture.cs create mode 100644 src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreTests.cs create mode 100644 src/Cedar.EventStore.Postgres.Tests/Properties/AssemblyInfo.cs create mode 100644 src/Cedar.EventStore.Postgres.Tests/packages.config create mode 100644 src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj create mode 100644 src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.nuspec create mode 100644 src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.v2.ncrunchproject create mode 100644 src/Cedar.EventStore.Postgres/CheckpointExtensions.cs create mode 100644 src/Cedar.EventStore.Postgres/InterlockedBoolean.cs create mode 100644 src/Cedar.EventStore.Postgres/InterlockedBooleanExtensions.cs create mode 100644 src/Cedar.EventStore.Postgres/PostgresEventStore.cs create mode 100644 src/Cedar.EventStore.Postgres/Properties/AssemblyInfo.cs create mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/CreateStream.sql create mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamAnyVersion.sql create mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamExpectedVersion.sql create mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/Dev.sql create mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/DropAll.sql create mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/GetVersion.sql create mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql create mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/ReadAllBackward.sql create mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/ReadAllForward.sql create mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamBackward.sql create mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamForward.sql create mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/Scripts.cs create mode 100644 src/Cedar.EventStore.Postgres/packages.config diff --git a/default.ps1 b/default.ps1 index bb86234ee..41a3a788c 100644 --- a/default.ps1 +++ b/default.ps1 @@ -47,6 +47,7 @@ task RunTests -depends Compile { Run-Tests "Cedar.EventStore.GetEventStore.Tests" Run-Tests "Cedar.EventStore.MsSql2008.Tests" Run-Tests "Cedar.EventStore.Sqlite.Tests" + Run-Tests "Cedar.EventStore.Postgres.Tests" } task ILMerge -depends Compile { @@ -69,6 +70,12 @@ task ILMerge -depends Compile { $inputDlls = "$dllDir\$mainDllName.dll" @( "EnsureThat" ) |% { $inputDlls = "$inputDlls $dllDir\$_.dll" } Invoke-Expression "$ilmergePath /targetplatform:v4 /internalize /allowDup /target:library /log /out:$mergedDir\$mainDllName.dll $inputDlls" + + $mainDllName = "Cedar.EventStore.Postgres" + $dllDir = "$srcDir\$mainDllName\bin\Release" + $inputDlls = "$dllDir\$mainDllName.dll" + @( "EnsureThat", "Npgsql" ) |% { $inputDlls = "$inputDlls $dllDir\$_.dll" } + Invoke-Expression "$ilmergePath /targetplatform:v4 /internalize /allowDup /target:library /log /out:$mergedDir\$mainDllName.dll $inputDlls" } task CreateNuGetPackages -depends ILMerge { diff --git a/src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.csproj b/src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.csproj new file mode 100644 index 000000000..5c6f32387 --- /dev/null +++ b/src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.csproj @@ -0,0 +1,99 @@ + + + + + + Debug + AnyCPU + {453FB5DB-99DC-42D3-9DFE-F81EDF98F5E3} + Library + Properties + Cedar.EventStore.Postgres.Tests + Cedar.EventStore.Postgres.Tests + v4.5.1 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\FluentAssertions.3.3.0\lib\net45\FluentAssertions.dll + True + + + ..\packages\FluentAssertions.3.3.0\lib\net45\FluentAssertions.Core.dll + True + + + ..\packages\Npgsql.3.1.0-unstable0000\lib\net45\Npgsql.dll + True + + + + + + + + + + ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True + + + ..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True + + + + + + + + + + + + + {148C90E9-0EA1-482E-94A9-F178294EFAC2} + Cedar.EventStore.Postgres + + + {F3FB96CF-4A3D-448D-A25E-6BC66E370BF6} + Cedar.EventStore.Tests + + + {3553E8E7-2C2A-45D5-BCB1-9AC7E5A209B2} + Cedar.EventStore + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.v2.ncrunchproject b/src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.v2.ncrunchproject new file mode 100644 index 0000000000000000000000000000000000000000..2ee0c8cbbe20200e8ac79b1ac3b630a98bdc8ed6 GIT binary patch literal 2838 zcmb_eO;6)M4E1>>{saGD;ez&1RmuVf4l62J#F-6g2{Z{xrhNSQUEcF_aMGlZv?z+y zPR4%Eo@aah{~O6fN-5+)5{bphDSqcNmsA$A!k1gDrMfnj1Nk9`ct-LY-%?r1om%sI ziT_UCWGu&Wq|Zoxe!9=R5%USQSKu(mTjI^ZCV~G*Dm;l~@)vk1epNEV{u{-7#J?wS z=&)Pr-{UFO((o6$5vrU!L)0hPOgCDrG+bE5?vw-=dS{{yvQ( zPcs3V0;qSrGrF0(UA-8O2pq`bMb2=tIoM>#K&+?2?0KCzu;uAdclTJ0o)li}L>XCLC zDu^0p&H4K0$`bz_UZQWRV;@h(=L)BrYyU`Ued1hLL1fa7w~s~Je*sT!Aj0Q1hT-ev z8M!R9{+X@N8=XwC7UN+zsL-8OS`*A?UE90*Fc$1XjFI({t+Utco12*ZEDRxV^d~b# zywsBKXqSkrjn=G^;r51IuYyF*ZP97A$Ep#22a#Idy=nc_M}Ty zWr;|APirFewJ{Xt-_H!zdA)PAXJDmsrfc5Sqx~L_#*i`b45?Z7G0{S GetEventStore() + { + Func createConnectionFunc = () => new NpgsqlConnection( + @"Server=127.0.0.1;Port=5432;Database=cedar_tests;User Id=postgres;Password=postgres;"); + + var eventStore = new PostgresEventStore(createConnectionFunc); + + await eventStore.DropAll(ignoreErrors: true); + await eventStore.InitializeStore(); + + return eventStore; + } + } +} \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreTests.cs b/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreTests.cs new file mode 100644 index 000000000..652c465e1 --- /dev/null +++ b/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreTests.cs @@ -0,0 +1,10 @@ +namespace Cedar.EventStore.Postgres.Tests +{ + public class PostgresEventStoreTests : EventStoreAcceptanceTests + { + protected override EventStoreAcceptanceTestFixture GetFixture() + { + return new PostgresEventStoreFixture(); + } + } +} \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres.Tests/Properties/AssemblyInfo.cs b/src/Cedar.EventStore.Postgres.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..996ba0a20 --- /dev/null +++ b/src/Cedar.EventStore.Postgres.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Cedar.EventStore.Postgres.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Cedar.EventStore.Postgres.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("dcf6bdff-8e51-46ae-9bcd-94a6ab78b26a")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Cedar.EventStore.Postgres.Tests/packages.config b/src/Cedar.EventStore.Postgres.Tests/packages.config new file mode 100644 index 000000000..be379f255 --- /dev/null +++ b/src/Cedar.EventStore.Postgres.Tests/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj new file mode 100644 index 000000000..73373066c --- /dev/null +++ b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj @@ -0,0 +1,94 @@ + + + + + Debug + AnyCPU + {148C90E9-0EA1-482E-94A9-F178294EFAC2} + Library + Properties + Cedar.EventStore.Postgres + Cedar.EventStore.Postgres + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Ensure.That.2.0.0\lib\portable-net4+sl5+netcore45+wpa81+wp8+MonoAndroid1+MonoTouch1\EnsureThat.dll\EnsureThat.dll + True + + + ..\packages\Npgsql.3.1.0-unstable0000\lib\net45\Npgsql.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {3553E8E7-2C2A-45D5-BCB1-9AC7E5A209B2} + Cedar.EventStore + + + + + \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.nuspec b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.nuspec new file mode 100644 index 000000000..0a4683089 --- /dev/null +++ b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.nuspec @@ -0,0 +1,29 @@ + + + + Cedar.EventStore.Postgres + 0.0.0.0 + Damian Hickey + Damian Hickey + https://github.com/damianh/Cedar.EventStore + https://github.com/damianh/Cedar.EventStore/blob/master/LICENSE + false + Cedar - Event Store - PostgreSQL + + PostgreSQL plugin for Cedar EventStore + + cqrs event-sourcing domain-driven-design postgres postgresql pgsql + + + + + + + + + + + + + + diff --git a/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.v2.ncrunchproject b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.v2.ncrunchproject new file mode 100644 index 0000000000000000000000000000000000000000..f14526cf83386d3d740ab04ba7a6314f48bd1326 GIT binary patch literal 2964 zcmb_e%Wm614CJ{${~>?S^it$t7*LZOawuZN4th>yIZb04Qv9l9hW)=4^9lc+ z!I68%u)V_`?k4tUd2RB{;Xeg0vb<83%vTKDm411_ml@s)*{GEL1gscCihcK;ERXkT z9C?}v*c3pW_s-~c?hf^0JR)!)i&we8$>v~_Ap^0V3bW^R=D?PxN8LSOHI}DMp7?Er zHJ;`1-ny7gf$Q}>gjZ8##_TX}UA&kZ<|xvv`(jL!{DH`o4=)cVAwu!6{>9d93tw*La2+(3l=HiqHr zNjhu@>WDH>l8^R$3FxXI$|{VWV2aP%iL zMZDCK?`YSEtc}*JlHvA-U9W;fF69jo#)ve`r5SiIYU-t)*CBR>b^%Y+J$sU`X!fLQ zRb`1teNSs5^|dh+=HJf@)_J{iv}a(YbEa$F)ua6!kH(NO@eHY1x0z@m*HpVhzJIJ| z{i$~8el~`2i!-a&zMl!*cMnd1A^UfuOb`*f{z7|UhA6sxw|ALGs=0s;-`97iP~n{P z4xQC|{CJmn^pTyz-z{pXgf`fpgAREQ!|IbdjJ;qSGeQTpea__@P7k+hc)feTUo~`J0?UYm9Zct>M<^)_juz_KmQ|-){X} e4H>EsIp?773B2i^fQQ+T)qc<1;nmRRmi-^Q0vlie literal 0 HcmV?d00001 diff --git a/src/Cedar.EventStore.Postgres/CheckpointExtensions.cs b/src/Cedar.EventStore.Postgres/CheckpointExtensions.cs new file mode 100644 index 000000000..bb21ab6b6 --- /dev/null +++ b/src/Cedar.EventStore.Postgres/CheckpointExtensions.cs @@ -0,0 +1,14 @@ +namespace Cedar.EventStore.Postgres +{ + internal static class CheckpointExtensions + { + internal static long GetOrdinal(this Checkpoint checkpoint) + { + if(ReferenceEquals(checkpoint, Checkpoint.Start)) + { + return -1; + } + return ReferenceEquals(checkpoint, Checkpoint.End) ? long.MaxValue : long.Parse(checkpoint.Value); + } + } +} \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/InterlockedBoolean.cs b/src/Cedar.EventStore.Postgres/InterlockedBoolean.cs new file mode 100644 index 000000000..304063631 --- /dev/null +++ b/src/Cedar.EventStore.Postgres/InterlockedBoolean.cs @@ -0,0 +1,72 @@ +// +// Copyright 2013 Hans Wolff +// +// Source: https://gist.github.com/hanswolff/7926751 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +namespace Cedar.EventStore.Postgres +{ + using System.Threading; + + /// + /// Interlocked support for boolean values + /// + internal class InterlockedBoolean + { + private int _value; + + /// + /// Current value + /// + public bool Value + { + get { return this._value == 1; } + } + + /// + /// Initializes a new instance of + /// + /// initial value + public InterlockedBoolean(bool initialValue = false) + { + this._value = initialValue ? 1 : 0; + } + + /// + /// Sets a new value + /// + /// new value + /// the original value before any operation was performed + public bool Set(bool newValue) + { + var oldValue = Interlocked.Exchange(ref this._value, newValue ? 1 : 0); + return oldValue == 1; + } + + /// + /// Compares the current value and the comparand for equality and, if they are equal, + /// replaces the current value with the new value in an atomic/thread-safe operation. + /// + /// new value + /// value to compare the current value with + /// the original value before any operation was performed + public bool CompareExchange(bool newValue, bool comparand) + { + var oldValue = Interlocked.CompareExchange(ref this._value, newValue ? 1 : 0, comparand ? 1 : 0); + return oldValue == 1; + } + } +} diff --git a/src/Cedar.EventStore.Postgres/InterlockedBooleanExtensions.cs b/src/Cedar.EventStore.Postgres/InterlockedBooleanExtensions.cs new file mode 100644 index 000000000..ae8be7978 --- /dev/null +++ b/src/Cedar.EventStore.Postgres/InterlockedBooleanExtensions.cs @@ -0,0 +1,10 @@ +namespace Cedar.EventStore.Postgres +{ + internal static class InterlockedBooleanExtensions + { + internal static bool EnsureCalledOnce(this InterlockedBoolean interlockedBoolean) + { + return interlockedBoolean.CompareExchange(true, false); + } + } +} \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs new file mode 100644 index 000000000..89f721f46 --- /dev/null +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -0,0 +1,481 @@ +namespace Cedar.EventStore.Postgres +{ + using System; + using System.Collections.Generic; + using System.Data; + using System.Data.SqlClient; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + + using Cedar.EventStore.Exceptions; + using Cedar.EventStore.Postgres.SqlScripts; + + using EnsureThat; + + using Npgsql; + + using NpgsqlTypes; + + public class PostgresEventStore : IEventStore + { + private readonly Func _createConnection; + + private readonly NpgsqlConnection connection; + private readonly InterlockedBoolean _isDisposed = new InterlockedBoolean(); + + public PostgresEventStore(Func createConnection) + { + Ensure.That(createConnection, "createConnection").IsNotNull(); + + _createConnection = createConnection; + connection = createConnection(); + connection.Open(); + } + + public async Task AppendToStream( + string streamId, + int expectedVersion, + IEnumerable events, + CancellationToken cancellationToken = default(CancellationToken)) + { + Ensure.That(streamId, "streamId").IsNotNullOrWhiteSpace(); + Ensure.That(expectedVersion, "expectedVersion").IsGte(-2); + Ensure.That(events, "events").IsNotNull(); + + var streamIdInfo = HashStreamId(streamId); + + //using(var connection = await this.OpenConnection(cancellationToken)) + using(var tx = connection.BeginTransaction(IsolationLevel.Serializable)) + { + int streamIdInternal = -1; + int currentVersion = expectedVersion; + bool isDeleted = false; + + if(expectedVersion == ExpectedVersion.NoStream) + { + using( + var command = + new NpgsqlCommand( + "INSERT INTO streams(id, id_original) VALUES (:stream_id, :stream_id_original) RETURNING id_internal;", + connection, + tx)) + { + command.Parameters.AddWithValue(":stream_id", streamIdInfo.StreamId); + command.Parameters.AddWithValue(":stream_id_original", streamIdInfo.StreamIdOriginal); + + streamIdInternal = + (int)await command.ExecuteScalarAsync(cancellationToken).NotOnCapturedContext(); + } + } + else + { + using(var command = new NpgsqlCommand(@" +SELECT streams.id_internal, + streams.is_deleted, + events.stream_version +FROM streams +LEFT JOIN events + ON events.stream_id_internal = streams.id_internal +WHERE streams.id = :stream_id +ORDER BY events.ordinal +LIMIT 1;", connection, tx)) + { + command.Parameters.AddWithValue(":stream_id", streamIdInfo.StreamId); + + using(var dr = await command.ExecuteReaderAsync().NotOnCapturedContext()) + { + while (await dr.ReadAsync().NotOnCapturedContext()) + { + streamIdInternal = dr.GetInt32(0); + isDeleted = dr.GetBoolean(1); + if(!isDeleted) + { + currentVersion = dr.GetInt32(2); + } + } + } + } + + if(isDeleted) + { + tx.Rollback(); + throw new StreamDeletedException(streamId); + } + + if(expectedVersion != ExpectedVersion.Any && currentVersion != expectedVersion) + { + tx.Rollback(); + throw new WrongExpectedVersionException(streamId, expectedVersion); + } + } + + using( + var writer = + connection.BeginBinaryImport( + "COPY events (stream_id_internal, stream_version, id, created, type, json_data, json_metadata) FROM STDIN BINARY") + ) + { + foreach(var @event in events) + { + if(cancellationToken.IsCancellationRequested) + { + writer.Cancel(); + tx.Rollback(); + } + + currentVersion++; + writer.StartRow(); + writer.Write(streamIdInternal, NpgsqlDbType.Integer); + writer.Write(currentVersion, NpgsqlDbType.Integer); + writer.Write(@event.EventId, NpgsqlDbType.Uuid); + writer.Write(SystemClock.GetUtcNow(), NpgsqlDbType.TimestampTZ); + writer.Write(@event.Type); + writer.Write(@event.JsonData, NpgsqlDbType.Json); + writer.Write(@event.JsonMetadata, NpgsqlDbType.Json); + } + } + + tx.Commit(); + } + } + + public Task DeleteStream( + string streamId, + int expectedVersion = ExpectedVersion.Any, + CancellationToken cancellationToken = default(CancellationToken)) + { + Ensure.That(streamId, "streamId").IsNotNullOrWhiteSpace(); + Ensure.That(expectedVersion, "expectedVersion").IsGte(-2); + + var streamIdInfo = HashStreamId(streamId); + + return expectedVersion == ExpectedVersion.Any + ? this.DeleteStreamAnyVersion(streamIdInfo.StreamId, cancellationToken) + : this.DeleteStreamExpectedVersion(streamIdInfo.StreamId, expectedVersion, cancellationToken); + } + + private async Task DeleteStreamAnyVersion( + string streamId, + CancellationToken cancellationToken) + { + //using (var connection = await this.OpenConnection(cancellationToken)) + using (var command = new NpgsqlCommand("delete_stream_any_version", connection)) + { + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue("stream_id", streamId); + await command + .ExecuteNonQueryAsync(cancellationToken) + .NotOnCapturedContext(); + } + } + + + private async Task DeleteStreamExpectedVersion( + string streamId, + int expectedVersion, + CancellationToken cancellationToken) + { + //using (var connection = await this.OpenConnection(cancellationToken)) + using (var command = new NpgsqlCommand("delete_stream_expected_version", connection)) + { + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue("stream_id", streamId); + command.Parameters.AddWithValue("expected_version", expectedVersion); + try + { + await command + .ExecuteNonQueryAsync(cancellationToken) + .NotOnCapturedContext(); + } + catch(NpgsqlException ex) + { + if(ex.MessageText == "WrongExpectedVersion") + { + throw new WrongExpectedVersionException(streamId, expectedVersion, ex); + } + throw; + } + } + } + + public async Task ReadAll( + Checkpoint checkpoint, + int maxCount, + ReadDirection direction = ReadDirection.Forward, + CancellationToken cancellationToken = default(CancellationToken)) + { + Ensure.That(checkpoint, "checkpoint").IsNotNull(); + Ensure.That(maxCount, "maxCount").IsGt(0); + + if(this._isDisposed.Value) + { + throw new ObjectDisposedException("PostgresEventStore"); + } + + long ordinal = checkpoint.GetOrdinal(); + + var commandText = direction == ReadDirection.Forward ? Scripts.ReadAllForward : Scripts.ReadAllBackward; + + //using (var connection = await this.OpenConnection(cancellationToken)) + using (var command = new NpgsqlCommand(commandText, connection)) + { + command.Parameters.AddWithValue(":ordinal", ordinal); + command.Parameters.AddWithValue(":count", maxCount + 1); //Read extra row to see if at end or not + + List streamEvents = new List(); + + using(var reader = await command.ExecuteReaderAsync(cancellationToken).NotOnCapturedContext()) + { + if (!reader.HasRows) + { + return new AllEventsPage(checkpoint.Value, + null, + true, + direction, + streamEvents.ToArray()); + } + + while (await reader.ReadAsync(cancellationToken).NotOnCapturedContext()) + { + var streamId = reader.GetString(0); + var StreamVersion = reader.GetInt32(1); + ordinal = reader.GetInt64(2); + var eventId = reader.GetGuid(3); + var created = reader.GetDateTime(4); + var type = reader.GetString(5); + var jsonData = reader.GetString(6); + var jsonMetadata = reader.GetString(7); + + var streamEvent = new StreamEvent(streamId, + eventId, + StreamVersion, + ordinal.ToString(), + created, + type, + jsonData, + jsonMetadata); + + streamEvents.Add(streamEvent); + } + } + + bool isEnd = true; + string nextCheckpoint = null; + + if(streamEvents.Count == maxCount + 1) + { + isEnd = false; + nextCheckpoint = streamEvents[maxCount].Checkpoint; + streamEvents.RemoveAt(maxCount); + } + + return new AllEventsPage(checkpoint.Value, + nextCheckpoint, + isEnd, + direction, + streamEvents.ToArray()); + } + } + + public async Task ReadStream( + string streamId, + int start, + int count, + ReadDirection direction = ReadDirection.Forward, + CancellationToken cancellationToken = default(CancellationToken)) + { + Ensure.That(streamId, "streamId").IsNotNull(); + Ensure.That(start, "start").IsGte(-1); + Ensure.That(count, "count").IsGte(0); + + var streamIdInfo = HashStreamId(streamId); + + var StreamVersion = start == StreamPosition.End ? int.MaxValue : start; + string commandText; + Func, int> getNextSequenceNumber; + if(direction == ReadDirection.Forward) + { + commandText = "read_stream_forward"; //Scripts.ReadStreamForward; + getNextSequenceNumber = events => events.Last().StreamVersion + 1; + } + else + { + commandText = "read_stream_backward"; //Scripts.ReadStreamBackward; + getNextSequenceNumber = events => events.Last().StreamVersion - 1; + } + + //using (var connection = await this.OpenConnection(cancellationToken)) + using (var tx = connection.BeginTransaction()) + using (var command = new NpgsqlCommand(commandText, connection, tx)) + { + command.CommandType = CommandType.StoredProcedure; + command.Parameters.AddWithValue(":stream_id", streamIdInfo.StreamId); + command.Parameters.AddWithValue(":count", count + 1); //Read extra row to see if at end or not + command.Parameters.AddWithValue(":stream_version", StreamVersion); + + List streamEvents = new List(); + + using(var reader = await command.ExecuteReaderAsync(cancellationToken).NotOnCapturedContext()) + { + await reader.ReadAsync(cancellationToken).NotOnCapturedContext(); + bool doesNotExist = reader.IsDBNull(0); + if(doesNotExist) + { + return new StreamEventsPage( + streamId, PageReadStatus.StreamNotFound, start, -1, -1, direction, isEndOfStream: true); + } + + + // Read IsDeleted result set + var isDeleted = reader.GetBoolean(1); + if(isDeleted) + { + return new StreamEventsPage( + streamId, PageReadStatus.StreamDeleted, 0, 0, 0, direction, isEndOfStream: true); + } + + + // Read Events result set + await reader.NextResultAsync(cancellationToken).NotOnCapturedContext(); + while(await reader.ReadAsync(cancellationToken).NotOnCapturedContext()) + { + var StreamVersion1 = reader.GetInt32(0); + var ordinal = reader.GetInt64(1); + var eventId = reader.GetGuid(2); + var created = reader.GetDateTime(3); + var type = reader.GetString(4); + var jsonData = reader.GetString(5); + var jsonMetadata = reader.GetString(6); + + var streamEvent = new StreamEvent( + streamId, eventId, StreamVersion1, ordinal.ToString(), created, type, jsonData, jsonMetadata); + + streamEvents.Add(streamEvent); + } + + // Read last event revision result set + await reader.NextResultAsync(cancellationToken).NotOnCapturedContext(); + await reader.ReadAsync(cancellationToken).NotOnCapturedContext(); + var lastStreamVersion = reader.GetInt32(0); + + + bool isEnd = true; + if(streamEvents.Count == count + 1) + { + isEnd = false; + streamEvents.RemoveAt(count); + } + + return new StreamEventsPage( + streamId, + PageReadStatus.Success, + start, + getNextSequenceNumber(streamEvents), + lastStreamVersion, + direction, + isEnd, + streamEvents.ToArray()); + } + } + } + + public void Dispose() + { + if(this._isDisposed.EnsureCalledOnce()) + { + return; + } + this.connection.Dispose(); + } + + public async Task InitializeStore( + bool ignoreErrors = false, + CancellationToken cancellationToken = default(CancellationToken)) + { + + //using (var connection = await this.OpenConnection(cancellationToken)) + using(var cmd = new NpgsqlCommand(Scripts.InitializeStore, connection)) + { + if (ignoreErrors) + { + await ExecuteAndIgnoreErrors(() => cmd.ExecuteNonQueryAsync(cancellationToken)) + .NotOnCapturedContext(); + } + else + { + await cmd.ExecuteNonQueryAsync(cancellationToken) + .NotOnCapturedContext(); + } + } + } + + public async Task DropAll( + bool ignoreErrors = false, + CancellationToken cancellationToken = default(CancellationToken)) + { + //using (var connection = await this.OpenConnection(cancellationToken)) + using(var cmd = new NpgsqlCommand(Scripts.DropAll, connection)) + { + if (ignoreErrors) + { + await ExecuteAndIgnoreErrors(() => cmd.ExecuteNonQueryAsync(cancellationToken)) + .NotOnCapturedContext(); + } + else + { + await cmd.ExecuteNonQueryAsync(cancellationToken) + .NotOnCapturedContext(); + } + } + } + + private static async Task ExecuteAndIgnoreErrors(Func> operation) + { + try + { + return await operation().NotOnCapturedContext(); + } + catch + { + return default(T); + } + } + + private static StreamIdInfo HashStreamId(string streamId) + { + Ensure.That(streamId, "streamId").IsNotNullOrWhiteSpace(); + + Guid _; + if(Guid.TryParse(streamId, out _)) + { + return new StreamIdInfo(streamId, streamId); + } + + byte[] hashBytes = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(streamId)); + var hashedStreamId = BitConverter.ToString(hashBytes).Replace("-", ""); + return new StreamIdInfo(hashedStreamId, streamId); + } + + private async Task OpenConnection(CancellationToken cancellationToken = default(CancellationToken)) + { + var connection = _createConnection(); + await connection.OpenAsync(cancellationToken); + return connection; + } + + private class StreamIdInfo + { + public readonly string StreamId; + public readonly string StreamIdOriginal; + + public StreamIdInfo(string streamId, string streamIdOriginal) + { + this.StreamId = streamId; + this.StreamIdOriginal = streamIdOriginal; + } + } + } +} \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/Properties/AssemblyInfo.cs b/src/Cedar.EventStore.Postgres/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..bf9ab5235 --- /dev/null +++ b/src/Cedar.EventStore.Postgres/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Cedar.EventStore.Postgres")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Cedar.EventStore.Postgres")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("3e2a4f51-0a30-4af3-9d8f-369d57b34486")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/CreateStream.sql b/src/Cedar.EventStore.Postgres/SqlScripts/CreateStream.sql new file mode 100644 index 000000000..d1b0ac63b --- /dev/null +++ b/src/Cedar.EventStore.Postgres/SqlScripts/CreateStream.sql @@ -0,0 +1,20 @@ +--SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; +--BEGIN TRANSACTION CreateStream; +-- integer stream_id_internal; +-- BEGIN +-- INSERT INTO dbo.Streams (Id, IdOriginal) VALUES (@streamId, @streamIdOriginal); +-- SELECT @streamIdInternal = SCOPE_IDENTITY(); + +-- INSERT INTO dbo.Events (StreamIdInternal, StreamVersion, Id, Created, [Type], JsonData, JsonMetadata) +-- SELECT @streamIdInternal, +-- StreamVersion, +-- Id, +-- Created, +-- [Type], +-- JsonData, +-- JsonMetadata +-- FROM @events; + +-- END; +-- SELECT @streamIdInternal; +--COMMIT TRANSACTION CreateStream; diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamAnyVersion.sql b/src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamAnyVersion.sql new file mode 100644 index 000000000..a28e8ba1d --- /dev/null +++ b/src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamAnyVersion.sql @@ -0,0 +1,15 @@ +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; +BEGIN TRANSACTION DeleteStream + DECLARE @streamIdInternal AS INT + + SELECT @streamIdInternal = Streams.IdInternal + FROM Streams + WHERE Streams.Id = @streamId; + + DELETE FROM Events + WHERE Events.StreamIdInternal = @streamIdInternal; + + UPDATE Streams + SET IsDeleted = '1' + WHERE Streams.Id = @streamId; +COMMIT TRANSACTION DeleteStream \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamExpectedVersion.sql b/src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamExpectedVersion.sql new file mode 100644 index 000000000..54ff874d7 --- /dev/null +++ b/src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamExpectedVersion.sql @@ -0,0 +1,37 @@ +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; +BEGIN TRANSACTION DeleteStream + DECLARE @streamIdInternal AS INT; + DECLARE @latestStreamVersion AS INT; + + SELECT @streamIdInternal = Streams.IdInternal + FROM Streams + WHERE Streams.Id = @streamId; + + IF @streamIdInternal IS NULL + BEGIN + ROLLBACK TRANSACTION DeleteStream; + RAISERROR('WrongExpectedVersion', 1,1); + RETURN; + END + + SELECT TOP(1) + @latestStreamVersion = Events.StreamVersion + FROM Events + WHERE Events.StreamIDInternal = @streamIdInternal + ORDER BY Events.Ordinal DESC; + + IF @latestStreamVersion != @expectedStreamVersion + BEGIN + ROLLBACK TRANSACTION DeleteStream; + RAISERROR('WrongExpectedVersion', 1,2); + RETURN; + END + + UPDATE Streams + SET IsDeleted = '1' + WHERE Streams.Id = @streamId ; + + DELETE FROM Events + WHERE Events.StreamIdInternal = @streamIdInternal; + +COMMIT TRANSACTION DeleteStream \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/Dev.sql b/src/Cedar.EventStore.Postgres/SqlScripts/Dev.sql new file mode 100644 index 000000000..3160e0320 --- /dev/null +++ b/src/Cedar.EventStore.Postgres/SqlScripts/Dev.sql @@ -0,0 +1,306 @@ +DROP TABLE IF EXISTS events; +DROP TABLE IF EXISTS streams; +DROP TYPE IF EXISTS new_stream_events; +CREATE TABLE streams( + id text NOT NULL, + id_original text NOT NULL, + id_internal SERIAL PRIMARY KEY NOT NULL, + is_deleted boolean DEFAULT (false) NOT NULL +); +CREATE UNIQUE INDEX ix_streams_id ON streams USING btree(id); + +CREATE TABLE events( + stream_id_internal integer NOT NULL, + stream_version integer NOT NULL, + ordinal SERIAL PRIMARY KEY NOT NULL , + id uuid NOT NULL, + created timestamp NOT NULL, + type text NOT NULL, + json_data json NOT NULL, + json_metadata json , + CONSTRAINT fk_events_streams FOREIGN KEY (stream_id_internal) REFERENCES streams(id_internal) +); + +CREATE UNIQUE INDEX ix_events_stream_id_internal_revision ON events USING btree(stream_id_internal, stream_version); + +CREATE TYPE new_stream_events AS ( + stream_version integer, + id uuid, + created timestamp, + type text, + json_data json, + json_metadata json +); + +-- ExpectedVersion.NoStream + +new_events new_stream_events; +stream_id text; + +stream_id = 'stream-1'; + + +INSERT INTO @newEvents + ( + Id , + [Type] , + JsonData , + JsonMetadata + ) VALUES + ('00000000-0000-0000-0000-000000000001', 'type1', '\"data1\"', '\"meta1\"'), + ('00000000-0000-0000-0000-000000000002', 'type2', '\"data2\"', '\"meta2\"'), + ('00000000-0000-0000-0000-000000000003', 'type3', '\"data3\"', '\"meta3\"'), + ('00000000-0000-0000-0000-000000000004', 'type4', '\"data4\"', '\"meta4\"'); + +-- Actual SQL statement of interest +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; +BEGIN TRANSACTION CreateStream; + DECLARE @count AS INT; + DECLARE @streamIdInternal AS INT; + BEGIN + INSERT INTO dbo.Streams (Id, IdOriginal) VALUES (@streamId, @streamId); + SELECT @streamIdInternal = SCOPE_IDENTITY(); + + INSERT INTO dbo.Events (StreamIdInternal, StreamVersion, Id, Created, [Type], JsonData, JsonMetadata) + SELECT @streamIdInternal, + StreamVersion, + Id, + Created, + [Type], + JsonData, + JsonMetadata + FROM @newEvents; + + END; + SELECT @streamIdInternal; +COMMIT TRANSACTION CreateStream; + + +SET @streamId = 'stream-2'; +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; +BEGIN TRANSACTION CreateStream; + BEGIN + INSERT INTO dbo.Streams (Id, IdOriginal) VALUES (@streamId, @streamId); + SELECT @streamIdInternal = SCOPE_IDENTITY(); + + INSERT INTO dbo.Events (StreamIdInternal, StreamVersion, Id, Created, [Type], JsonData, JsonMetadata) + SELECT @streamIdInternal, + StreamVersion, + Id, + Created, + [Type], + JsonData, + JsonMetadata + FROM @newEvents + + END; + SELECT @streamIdInternal; +COMMIT TRANSACTION CreateStream; + +SELECT * FROM dbo.Streams; +SELECT * FROM dbo.Events; + +DECLARE @pageNumber AS INT, @rowspPage AS INT; +SET @pageNumber = 2; +SET @rowspPage = 5; + +/* SQL Server 2012+ */ + SELECT Streams.IdOriginal As StreamId, + Events.StreamVersion, + Events.Ordinal, + Events.Id AS EventId, + Events.Created, + Events.Type, + Events.JsonData, + Events.JsonMetadata + FROM Events + INNER JOIN Streams + ON Events.StreamIdInternal=Streams.IdInternal + ORDER BY Events.Ordinal + OFFSET ((@pageNumber - 1) * @rowspPage) ROWS + FETCH NEXT @rowspPage ROWS ONLY; + + /* SQL Server 2000+ */ + SELECT Id As StreamId, + StreamVersion, + Ordinal, + EventId, + Created, + [Type], + JsonData, + JsonMetadata + FROM ( + SELECT ROW_NUMBER() OVER(ORDER BY Events.Ordinal) AS NUMBER, + Events.StreamIdInternal, + Events.StreamVersion, + Events.Ordinal, + Events.Id AS EventId, + Events.Created, + Events.Type, + Events.JsonData, + Events.JsonMetadata + FROM Events + ) AS PageTable + INNER JOIN Streams + ON StreamIdInternal=Streams.IdInternal + WHERE NUMBER BETWEEN ((@pageNumber - 1) * @RowspPage + 1) AND (@pageNumber * @rowspPage) + ORDER BY NUMBER; + +DECLARE @ordinal AS INT, @count1 AS INT; +SET @ordinal = 2; +SET @count1 = 5 + +/* SQL Server 2008+ */ + SELECT TOP(@count1) + Streams.IdOriginal As StreamId, + Events.StreamVersion, + Events.Ordinal, + Events.Id AS EventId, + Events.Created, + Events.Type, + Events.JsonData, + Events.JsonMetadata + FROM Events + INNER JOIN Streams + ON Events.StreamIdInternal=Streams.IdInternal + WHERE Events.Ordinal >= @ordinal + ORDER BY Events.Ordinal; + + /* SQL Server 2008+ */ + SELECT TOP(@count1) + Streams.IdOriginal As StreamId, + Events.StreamVersion, + Events.Ordinal, + Events.Id AS EventId, + Events.Created, + Events.Type, + Events.JsonData, + Events.JsonMetadata + FROM Events + INNER JOIN Streams + ON Events.StreamIdInternal=Streams.IdInternal + WHERE Events.Ordinal <= @ordinal + ORDER BY Events.Ordinal DESC; + +/* Delete Streeam*/ +BEGIN TRANSACTION DeleteStream + SELECT @streamIdInternal = Streams.IdInternal + FROM Streams + WHERE Streams.Id = @streamId; + + DELETE FROM Events + WHERE Events.StreamIdInternal = @streamIdInternal; + + UPDATE Streams + SET IsDeleted = '1' + WHERE Streams.Id = @streamId; +COMMIT TRANSACTION DeleteStream + +SELECT * FROM dbo.Streams; +SELECT * FROM dbo.Events; + +/* ReadStreamForward */ + +SET @count = 5; +SET @streamId = 'stream-1'; +DECLARE @StreamVersion AS INT = 0 +DECLARE @isDeleted AS BIT; + + SELECT @streamIdInternal = Streams.IdInternal, + @isDeleted = Streams.IsDeleted + FROM Streams + WHERE Streams.Id = @streamId + + SELECT @isDeleted AS IsDeleted + + SELECT TOP(@count) + Events.StreamVersion, + Events.Ordinal, + Events.Id AS EventId, + Events.Created, + Events.Type, + Events.JsonData, + Events.JsonMetadata + FROM Events + INNER JOIN Streams + ON Events.StreamIdInternal = Streams.IdInternal + WHERE Events.StreamIDInternal = @streamIDInternal AND Events.StreamVersion >= @StreamVersion + ORDER BY Events.Ordinal; + + SELECT TOP(1) + Events.StreamVersion + FROM Events + WHERE Events.StreamIDInternal = @streamIDInternal + ORDER BY Events.Ordinal DESC; + +/* ReadStreamBackward */ + +SET @StreamVersion = 5; + + SELECT @streamIdInternal = Streams.IdInternal, + @isDeleted = Streams.IsDeleted + FROM Streams + WHERE Streams.Id = @streamId + + SELECT @isDeleted; + + SELECT TOP(@count) + Streams.IdOriginal As StreamId, + Streams.IsDeleted as IsDeleted, + Events.StreamVersion, + Events.Ordinal, + Events.Id AS EventId, + Events.Created, + Events.Type, + Events.JsonData, + Events.JsonMetadata + FROM Events + INNER JOIN Streams + ON Events.StreamIdInternal = Streams.IdInternal + WHERE Events.StreamIDInternal = @streamIDInternal AND Events.StreamVersion <= @StreamVersion + ORDER BY Events.Ordinal DESC + + SELECT TOP(1) + Events.StreamVersion + FROM Events + WHERE Events.StreamIDInternal = @streamIDInternal + ORDER BY Events.Ordinal DESC; + +/* Delete Stream with expected version */ +SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; +BEGIN TRANSACTION DeleteStream + DECLARE @streamIdInternal2 AS INT; + DECLARE @expectedStreamVersion AS INT = 3; + DECLARE @latestStreamVersion AS INT; + SET @streamId = 'stream-1'; + + SELECT @streamIdInternal2 = Streams.IdInternal + FROM Streams + WHERE Streams.Id = @streamId; + + IF @streamIdInternal2 IS NULL + BEGIN + ROLLBACK TRANSACTION DeleteStream; + RAISERROR('WrongExpectedVersion', 12,1); + END + + SELECT TOP(1) + @latestStreamVersion = Events.StreamVersion + FROM Events + WHERE Events.StreamIDInternal = @streamIdInternal2 + ORDER BY Events.Ordinal DESC; + + IF @latestStreamVersion != @expectedStreamVersion + BEGIN + ROLLBACK TRANSACTION DeleteStream; + RAISERROR('WrongExpectedVersion', 12,2); + END + + UPDATE Streams + SET IsDeleted = '1' + WHERE Streams.Id = @streamId ; + + DELETE FROM Events + WHERE Events.StreamIdInternal = @streamIdInternal2; + +COMMIT TRANSACTION DeleteStream \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/DropAll.sql b/src/Cedar.EventStore.Postgres/SqlScripts/DropAll.sql new file mode 100644 index 000000000..1c7a1c164 --- /dev/null +++ b/src/Cedar.EventStore.Postgres/SqlScripts/DropAll.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS events; +DROP TABLE IF EXISTS streams; +DROP TYPE IF EXISTS new_stream_events; \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/GetVersion.sql b/src/Cedar.EventStore.Postgres/SqlScripts/GetVersion.sql new file mode 100644 index 000000000..ba9e80e5f --- /dev/null +++ b/src/Cedar.EventStore.Postgres/SqlScripts/GetVersion.sql @@ -0,0 +1,3 @@ +DECLARE @ver nvarchar(128) +SET @ver = CAST(serverproperty('ProductVersion') AS nvarchar) +SET @ver = SUBSTRING(@ver, 1, CHARINDEX('.', @ver) - 1) \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql new file mode 100644 index 000000000..55cf83bef --- /dev/null +++ b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql @@ -0,0 +1,197 @@ +CREATE TABLE streams( + id_internal SERIAL PRIMARY KEY, + id text NOT NULL, + id_original text NOT NULL, + is_deleted boolean DEFAULT (false) NOT NULL +); +CREATE UNIQUE INDEX ix_streams_id ON streams USING btree(id); + +CREATE TABLE events( + stream_id_internal integer NOT NULL, + stream_version integer NOT NULL, + ordinal SERIAL PRIMARY KEY NOT NULL , + id uuid NOT NULL, + created timestamp NOT NULL, + type text NOT NULL, + json_data json NOT NULL, + json_metadata json , + CONSTRAINT fk_events_streams FOREIGN KEY (stream_id_internal) REFERENCES streams(id_internal) +); + +CREATE UNIQUE INDEX ix_events_stream_id_internal_revision ON events USING btree(stream_id_internal, stream_version); + +CREATE TYPE new_stream_events AS ( + stream_version integer, + id uuid, + created timestamp, + type text, + json_data json, + json_metadata json +); + +CREATE OR REPLACE FUNCTION read_stream_forward(_stream_id text, _count integer, _stream_version integer) RETURNS SETOF refcursor AS +$BODY$ +DECLARE + ref1 refcursor; + ref2 refcursor; + ref3 refcursor; + _stream_id_internal integer; + _is_deleted boolean; +BEGIN + +SELECT streams.id_internal, streams.is_deleted + INTO _stream_id_internal, _is_deleted + FROM streams + WHERE streams.id = _stream_id; + +OPEN ref1 FOR + SELECT _stream_id_internal, _is_deleted; +RETURN NEXT ref1; + + +OPEN ref2 FOR + SELECT + events.stream_version, + events.ordinal, + events.id AS event_id, + events.created, + events.type, + events.json_data, + events.json_metadata + FROM events + INNER JOIN streams + ON events.stream_id_internal = streams.id_internal + WHERE events.stream_id_internal = _stream_id_internal + AND events.stream_version >= _stream_version + ORDER BY events.ordinal + LIMIT _count; +RETURN next ref2; + +OPEN ref3 FOR + SELECT events.stream_version + FROM events + WHERE events.stream_id_internal = _stream_id_internal + ORDER BY events.ordinal DESC + LIMIT 1; +RETURN next ref3; + +RETURN; +END; +$BODY$ +LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION read_stream_backward(_stream_id text, _count integer, _stream_version integer) RETURNS SETOF refcursor AS +$BODY$ +DECLARE + ref1 refcursor; + ref2 refcursor; + ref3 refcursor; + _stream_id_internal integer; + _is_deleted boolean; +BEGIN + +SELECT streams.id_internal, streams.is_deleted + INTO _stream_id_internal, _is_deleted + FROM streams + WHERE streams.id = _stream_id; + +OPEN ref1 FOR + SELECT _stream_id_internal, _is_deleted; +RETURN NEXT ref1; + + +OPEN ref2 FOR + SELECT + events.stream_version, + events.ordinal, + events.id AS event_id, + events.created, + events.type, + events.json_data, + events.json_metadata + FROM events + INNER JOIN streams + ON events.stream_id_internal = streams.id_internal + WHERE events.stream_id_internal = _stream_id_internal + AND events.stream_version <= _stream_version + ORDER BY events.ordinal DESC + LIMIT _count; +RETURN next ref2; + +OPEN ref3 FOR + SELECT events.stream_version + FROM events + WHERE events.stream_id_internal = _stream_id_internal + ORDER BY events.ordinal DESC + LIMIT 1; +RETURN next ref3; + +RETURN; +END; +$BODY$ +LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION delete_stream_any_version(stream_id text) RETURNS VOID AS +$BODY$ +DECLARE + _stream_id_internal integer; +BEGIN + + SELECT streams.id_internal + INTO _stream_id_internal + FROM streams + WHERE streams.id = stream_id; + + DELETE FROM events + WHERE events.stream_id_internal = _stream_id_internal; + + UPDATE streams + SET is_deleted = true + WHERE streams.id_internal = _stream_id_internal; + +RETURN; +END; +$BODY$ +LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION delete_stream_expected_version(stream_id text, expected_version integer) RETURNS VOID AS +$BODY$ +DECLARE + _stream_id_internal integer; + _lastest_stream_version integer; +BEGIN + + SELECT streams.id_internal + INTO _stream_id_internal + FROM streams + WHERE streams.id = stream_id; + + IF _stream_id_internal IS NULL THEN + RAISE EXCEPTION 'WrongExpectedVersion'; + END IF; + + SELECT stream_version + INTO _lastest_stream_version + FROM events + WHERE events.stream_id_internal = _stream_id_internal + ORDER BY events.ordinal DESC + LIMIT 1; + + --IF (_lastest_stream_version <> expected_version) THEN + --BEGIN + -- RAISE EXCEPTION 'WrongExpectedVersion'; + --END IF; + + DELETE FROM events + WHERE events.stream_id_internal = _stream_id_internal; + + UPDATE streams + SET is_deleted = true + WHERE streams.id_internal = _stream_id_internal; + +RETURN; +END; +$BODY$ +LANGUAGE plpgsql; \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/ReadAllBackward.sql b/src/Cedar.EventStore.Postgres/SqlScripts/ReadAllBackward.sql new file mode 100644 index 000000000..8430a8be2 --- /dev/null +++ b/src/Cedar.EventStore.Postgres/SqlScripts/ReadAllBackward.sql @@ -0,0 +1,15 @@ +SELECT + streams.id_original As stream_id, + events.stream_version, + events.ordinal, + events.id AS event_id, + events.created, + events.type, + events.json_data, + events.json_metadata + FROM events + INNER JOIN streams + ON events.stream_id_internal = streams.id_internal + WHERE events.ordinal <= :ordinal + ORDER BY events.ordinal DESC +LIMIT :count \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/ReadAllForward.sql b/src/Cedar.EventStore.Postgres/SqlScripts/ReadAllForward.sql new file mode 100644 index 000000000..08b6ec1ee --- /dev/null +++ b/src/Cedar.EventStore.Postgres/SqlScripts/ReadAllForward.sql @@ -0,0 +1,15 @@ +SELECT + streams.id_original As stream_id, + events.stream_version, + events.ordinal, + events.id AS event_id, + events.created, + events.type, + events.json_data, + events.json_metadata + FROM events + INNER JOIN streams + ON events.stream_id_internal = streams.id_internal + WHERE events.ordinal >= :ordinal + ORDER BY events.ordinal +LIMIT :count \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamBackward.sql b/src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamBackward.sql new file mode 100644 index 000000000..17898f621 --- /dev/null +++ b/src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamBackward.sql @@ -0,0 +1,31 @@ +/* SQL Server 2008+ */ + + DECLARE @streamIdInternal AS INT + DECLARE @isDeleted AS BIT + + SELECT @streamIdInternal = Streams.IdInternal, + @isDeleted = Streams.IsDeleted + FROM Streams + WHERE Streams.Id = @streamId + + SELECT @isDeleted; + + SELECT TOP(@count) + Events.StreamVersion, + Events.Ordinal, + Events.Id AS EventId, + Events.Created, + Events.Type, + Events.JsonData, + Events.JsonMetadata + FROM Events + INNER JOIN Streams + ON Events.StreamIdInternal = Streams.IdInternal + WHERE Events.StreamIDInternal = @streamIDInternal AND Events.StreamVersion <= @StreamVersion + ORDER BY Events.Ordinal DESC + + SELECT TOP(1) + Events.StreamVersion + FROM Events + WHERE Events.StreamIDInternal = @streamIDInternal + ORDER BY Events.Ordinal DESC; \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamForward.sql b/src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamForward.sql new file mode 100644 index 000000000..f7fd32d78 --- /dev/null +++ b/src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamForward.sql @@ -0,0 +1,32 @@ +/* SQL Server 2008+ */ + + DECLARE @streamIdInternal AS INT + DECLARE @isDeleted AS BIT + + SELECT @streamIdInternal = Streams.IdInternal, + @isDeleted = Streams.IsDeleted + FROM Streams + WHERE Streams.Id = @streamId + + SELECT @isDeleted; + + SELECT + events.stream_version, + events.ordinal, + events.id AS event_id, + events.created, + events.type, + events.json_data, + events.json_metadata + FROM events + INNER JOIN streams + ON events.stream_id_internal = streams.id_internal + WHERE events.stream_id_internal = :stream_id_internal AND events.stream_version >= :stream_version + ORDER BY events.Ordinal + LIMIT :count; + + SELECT events.StreamVersion + FROM events + WHERE events.StreamIDInternal = @streamIDInternal + ORDER BY events.Ordinal DESC + LIMIT 1; \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/Scripts.cs b/src/Cedar.EventStore.Postgres/SqlScripts/Scripts.cs new file mode 100644 index 000000000..c0e3b397d --- /dev/null +++ b/src/Cedar.EventStore.Postgres/SqlScripts/Scripts.cs @@ -0,0 +1,78 @@ +namespace Cedar.EventStore.Postgres.SqlScripts +{ + using System; + using System.Collections.Concurrent; + using System.IO; + + public static class Scripts + { + private static readonly ConcurrentDictionary s_scripts + = new ConcurrentDictionary(); + + public static string InitializeStore + { + get { return GetScript("InitializeStore"); } + } + + public static string DropAll + { + get { return GetScript("DropAll"); } + } + + public static string CreateStream + { + get { return GetScript("CreateStream"); } + } + + public static string DeleteStreamAnyVersion + { + get { return GetScript("DeleteStreamAnyVersion"); } + } + + public static string DeleteStreamExpectedVersion + { + get { return GetScript("DeleteStreamExpectedVersion"); } + } + + public static string ReadAllForward + { + get { return GetScript("ReadAllForward"); } + } + + public static string ReadAllBackward + { + get { return GetScript("ReadAllBackward"); } + } + + public static string ReadStreamForward + { + get { return GetScript("ReadStreamForward"); } + } + + public static string ReadStreamBackward + { + get { return GetScript("ReadStreamBackward"); } + } + + private static string GetScript(string name) + { + return s_scripts.GetOrAdd(name, + key => + { + using(Stream stream = typeof(Scripts) + .Assembly + .GetManifestResourceStream("Cedar.EventStore.Postgres.SqlScripts." + key + ".sql")) + { + if(stream == null) + { + throw new Exception("Embedded resource not found. BUG!"); + } + using(StreamReader reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } + }); + } + } +} \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/packages.config b/src/Cedar.EventStore.Postgres/packages.config new file mode 100644 index 000000000..aa85ed273 --- /dev/null +++ b/src/Cedar.EventStore.Postgres/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/Cedar.EventStore.sln b/src/Cedar.EventStore.sln index 9d5d44b36..082a04fc6 100644 --- a/src/Cedar.EventStore.sln +++ b/src/Cedar.EventStore.sln @@ -18,6 +18,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cedar.EventStore.MsSql2008" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cedar.EventStore.MsSql2008.Tests", "Cedar.EventStore.MsSql2008.Tests\Cedar.EventStore.MsSql2008.Tests.csproj", "{97AA016B-0B9F-44C2-8228-A13B4E251FB0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cedar.EventStore.Postgres", "Cedar.EventStore.Postgres\Cedar.EventStore.Postgres.csproj", "{148C90E9-0EA1-482E-94A9-F178294EFAC2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cedar.EventStore.Postgres.Tests", "Cedar.EventStore.Postgres.Tests\Cedar.EventStore.Postgres.Tests.csproj", "{453FB5DB-99DC-42D3-9DFE-F81EDF98F5E3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -56,6 +60,14 @@ Global {97AA016B-0B9F-44C2-8228-A13B4E251FB0}.Debug|Any CPU.Build.0 = Debug|Any CPU {97AA016B-0B9F-44C2-8228-A13B4E251FB0}.Release|Any CPU.ActiveCfg = Release|Any CPU {97AA016B-0B9F-44C2-8228-A13B4E251FB0}.Release|Any CPU.Build.0 = Release|Any CPU + {148C90E9-0EA1-482E-94A9-F178294EFAC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {148C90E9-0EA1-482E-94A9-F178294EFAC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {148C90E9-0EA1-482E-94A9-F178294EFAC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {148C90E9-0EA1-482E-94A9-F178294EFAC2}.Release|Any CPU.Build.0 = Release|Any CPU + {453FB5DB-99DC-42D3-9DFE-F81EDF98F5E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {453FB5DB-99DC-42D3-9DFE-F81EDF98F5E3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {453FB5DB-99DC-42D3-9DFE-F81EDF98F5E3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {453FB5DB-99DC-42D3-9DFE-F81EDF98F5E3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From a9208b4a836c0cb28f482422065f6dde2b10ef79 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Fri, 19 Jun 2015 14:49:05 +0100 Subject: [PATCH 02/34] Add SharedAssemblyInfo.cs --- .../Cedar.EventStore.Postgres.csproj | 3 ++ .../Properties/AssemblyInfo.cs | 34 +------------------ 2 files changed, 4 insertions(+), 33 deletions(-) diff --git a/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj index 73373066c..e26dc594b 100644 --- a/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj +++ b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj @@ -47,6 +47,9 @@ + + Properties\SharedAssemblyInfo.cs + diff --git a/src/Cedar.EventStore.Postgres/Properties/AssemblyInfo.cs b/src/Cedar.EventStore.Postgres/Properties/AssemblyInfo.cs index bf9ab5235..240c04cf7 100644 --- a/src/Cedar.EventStore.Postgres/Properties/AssemblyInfo.cs +++ b/src/Cedar.EventStore.Postgres/Properties/AssemblyInfo.cs @@ -1,36 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. [assembly: AssemblyTitle("Cedar.EventStore.Postgres")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Cedar.EventStore.Postgres")] -[assembly: AssemblyCopyright("Copyright © 2015")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("3e2a4f51-0a30-4af3-9d8f-369d57b34486")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyDescription("")] \ No newline at end of file From a8e7b44d2e32440c17ab3fd22667ff5bbfd1da03 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Fri, 19 Jun 2015 14:49:18 +0100 Subject: [PATCH 03/34] Compact json stub data --- .../EventStoreAcceptanceTests.DeleteStream.cs | 23 ++++++++++++------- .../EventStoreAcceptanceTests.cs | 4 ++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.DeleteStream.cs b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.DeleteStream.cs index 47df84e7d..c00e1e8f4 100644 --- a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.DeleteStream.cs +++ b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.DeleteStream.cs @@ -18,8 +18,8 @@ public async Task When_delete_existing_stream_with_no_expected_version_then_shou const string streamId = "stream"; var events = new[] { - new NewStreamEvent(Guid.NewGuid(), "data", "meta"), - new NewStreamEvent(Guid.NewGuid(), "data", "meta") + new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\""), + new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\"") }; await eventStore.AppendToStream(streamId, ExpectedVersion.NoStream, events); @@ -40,7 +40,7 @@ public async Task When_delete_stream_that_does_not_exist() { using (var eventStore = await fixture.GetEventStore()) { - const string streamId = "stream"; + const string streamId = "notexist"; Func act = () => eventStore.DeleteStream(streamId); act.ShouldNotThrow(); @@ -58,8 +58,8 @@ public async Task When_delete_stream_with_a_matching_expected_version_then_shoul const string streamId = "stream"; var events = new[] { - new NewStreamEvent(Guid.NewGuid(), "data", "meta"), - new NewStreamEvent(Guid.NewGuid(), "data", "meta") + new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\""), + new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\"") }; await eventStore.AppendToStream(streamId, ExpectedVersion.NoStream, events); @@ -80,7 +80,14 @@ public async Task When_delete_stream_that_does_not_exist_with_expected_version_t { using (var eventStore = await fixture.GetEventStore()) { - const string streamId = "notexist"; + const string streamId = "stream"; + var events = new[] + { + new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\""), + new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\"") + }; + + await eventStore.AppendToStream(streamId, ExpectedVersion.NoStream, events); await eventStore.DeleteStream(streamId, 10) .ShouldThrow(); @@ -98,8 +105,8 @@ public async Task When_delete_stream_with_a_non_matching_expected_version_then_s const string streamId = "stream"; var events = new[] { - new NewStreamEvent(Guid.NewGuid(), "data", "meta"), - new NewStreamEvent(Guid.NewGuid(), "data", "meta") + new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\""), + new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\"") }; await eventStore.AppendToStream(streamId, ExpectedVersion.NoStream, events); diff --git a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.cs b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.cs index 4e025ad7f..bfa3e7f07 100644 --- a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.cs +++ b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.cs @@ -16,7 +16,7 @@ private static NewStreamEvent[] CreateNewStreamEvents(params int[] eventNumbers) .Select(eventNumber => { var eventId = Guid.Parse("00000000-0000-0000-0000-" + eventNumber.ToString().PadLeft(12, '0')); - return new NewStreamEvent(eventId, "type", "data", "metadata"); + return new NewStreamEvent(eventId, "type", "\"data\"", "\"headers\""); }) .ToArray(); } @@ -28,7 +28,7 @@ private static StreamEvent ExpectedStreamEvent( DateTime created) { var eventId = Guid.Parse("00000000-0000-0000-0000-" + eventNumber.ToString().PadLeft(12, '0')); - return new StreamEvent(streamId, eventId, sequenceNumber, null, created, "type", "data", "metadata"); + return new StreamEvent(streamId, eventId, sequenceNumber, null, created, "type", "\"data\"", "\"headers\""); } [Fact] From c80de7ad436c37f7c627e7a5d8ce8f25a4ced145 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Fri, 19 Jun 2015 15:06:01 +0100 Subject: [PATCH 04/34] Fix syntax for delete with expected version --- .../SqlScripts/InitializeStore.sql | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql index 55cf83bef..4eca42613 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql +++ b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql @@ -179,10 +179,9 @@ BEGIN ORDER BY events.ordinal DESC LIMIT 1; - --IF (_lastest_stream_version <> expected_version) THEN - --BEGIN - -- RAISE EXCEPTION 'WrongExpectedVersion'; - --END IF; + IF (_lastest_stream_version <> expected_version) THEN + RAISE EXCEPTION 'WrongExpectedVersion'; + END IF; DELETE FROM events WHERE events.stream_id_internal = _stream_id_internal; From 7c2686fd426edb9413a5bb1354d1657d4fee23e4 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Fri, 19 Jun 2015 16:31:06 +0100 Subject: [PATCH 05/34] Move logic to sprocs, where possible - read_all_forwards/backwards seems to hang when executed in a function, need to investigate. --- .../Cedar.EventStore.Postgres.csproj | 11 +- .../PostgresEventStore.cs | 64 +++-------- .../SqlScripts/BulkCopyEvents.sql | 2 + .../SqlScripts/CreateStream.sql | 23 +--- .../SqlScripts/DeleteStreamAnyVersion.sql | 15 --- .../DeleteStreamExpectedVersion.sql | 37 ------ .../SqlScripts/GetStream.sql | 9 ++ .../SqlScripts/GetVersion.sql | 3 - .../SqlScripts/InitializeStore.sql | 106 +++++++++++++++--- .../SqlScripts/ReadStreamBackward.sql | 31 ----- .../SqlScripts/ReadStreamForward.sql | 32 ------ .../SqlScripts/Scripts.cs | 67 +++++++---- 12 files changed, 168 insertions(+), 232 deletions(-) create mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/BulkCopyEvents.sql delete mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamAnyVersion.sql delete mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamExpectedVersion.sql create mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/GetStream.sql delete mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/GetVersion.sql delete mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamBackward.sql delete mode 100644 src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamForward.sql diff --git a/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj index e26dc594b..4603e832e 100644 --- a/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj +++ b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj @@ -65,19 +65,12 @@ - - - - - - + - - + - diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs index 89f721f46..f85bd9ea5 100644 --- a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Data; - using System.Data.SqlClient; using System.Linq; using System.Security.Cryptography; using System.Text; @@ -47,7 +46,6 @@ public async Task AppendToStream( var streamIdInfo = HashStreamId(streamId); - //using(var connection = await this.OpenConnection(cancellationToken)) using(var tx = connection.BeginTransaction(IsolationLevel.Serializable)) { int streamIdInternal = -1; @@ -56,12 +54,7 @@ public async Task AppendToStream( if(expectedVersion == ExpectedVersion.NoStream) { - using( - var command = - new NpgsqlCommand( - "INSERT INTO streams(id, id_original) VALUES (:stream_id, :stream_id_original) RETURNING id_internal;", - connection, - tx)) + using(var command = new NpgsqlCommand(Scripts.Functions.CreateStream, connection, tx){ CommandType = CommandType.StoredProcedure}) { command.Parameters.AddWithValue(":stream_id", streamIdInfo.StreamId); command.Parameters.AddWithValue(":stream_id_original", streamIdInfo.StreamIdOriginal); @@ -72,16 +65,7 @@ public async Task AppendToStream( } else { - using(var command = new NpgsqlCommand(@" -SELECT streams.id_internal, - streams.is_deleted, - events.stream_version -FROM streams -LEFT JOIN events - ON events.stream_id_internal = streams.id_internal -WHERE streams.id = :stream_id -ORDER BY events.ordinal -LIMIT 1;", connection, tx)) + using (var command = new NpgsqlCommand(Scripts.Functions.GetStream, connection, tx) { CommandType = CommandType.StoredProcedure }) { command.Parameters.AddWithValue(":stream_id", streamIdInfo.StreamId); @@ -101,21 +85,18 @@ ORDER BY events.ordinal if(isDeleted) { - tx.Rollback(); throw new StreamDeletedException(streamId); } if(expectedVersion != ExpectedVersion.Any && currentVersion != expectedVersion) { - tx.Rollback(); throw new WrongExpectedVersionException(streamId, expectedVersion); } } using( var writer = - connection.BeginBinaryImport( - "COPY events (stream_id_internal, stream_version, id, created, type, json_data, json_metadata) FROM STDIN BINARY") + connection.BeginBinaryImport(Scripts.BulkCopyEvents) ) { foreach(var @event in events) @@ -161,10 +142,8 @@ private async Task DeleteStreamAnyVersion( string streamId, CancellationToken cancellationToken) { - //using (var connection = await this.OpenConnection(cancellationToken)) - using (var command = new NpgsqlCommand("delete_stream_any_version", connection)) + using (var command = new NpgsqlCommand(Scripts.Functions.DeleteStreamAnyVersion, connection) { CommandType = CommandType.StoredProcedure }) { - command.CommandType = CommandType.StoredProcedure; command.Parameters.AddWithValue("stream_id", streamId); await command .ExecuteNonQueryAsync(cancellationToken) @@ -178,10 +157,8 @@ private async Task DeleteStreamExpectedVersion( int expectedVersion, CancellationToken cancellationToken) { - //using (var connection = await this.OpenConnection(cancellationToken)) - using (var command = new NpgsqlCommand("delete_stream_expected_version", connection)) + using (var command = new NpgsqlCommand(Scripts.Functions.DeleteStreamExpectedVersion, connection) { CommandType = CommandType.StoredProcedure }) { - command.CommandType = CommandType.StoredProcedure; command.Parameters.AddWithValue("stream_id", streamId); command.Parameters.AddWithValue("expected_version", expectedVersion); try @@ -219,11 +196,10 @@ public async Task ReadAll( var commandText = direction == ReadDirection.Forward ? Scripts.ReadAllForward : Scripts.ReadAllBackward; - //using (var connection = await this.OpenConnection(cancellationToken)) - using (var command = new NpgsqlCommand(commandText, connection)) + using (var command = new NpgsqlCommand(commandText, connection))// { CommandType = CommandType.StoredProcedure }) { - command.Parameters.AddWithValue(":ordinal", ordinal); - command.Parameters.AddWithValue(":count", maxCount + 1); //Read extra row to see if at end or not + command.Parameters.AddWithValue(":ordinal", NpgsqlDbType.Bigint, ordinal); + command.Parameters.AddWithValue(":count", NpgsqlDbType.Integer, maxCount + 1); //Read extra row to see if at end or not List streamEvents = new List(); @@ -298,23 +274,21 @@ public async Task ReadStream( Func, int> getNextSequenceNumber; if(direction == ReadDirection.Forward) { - commandText = "read_stream_forward"; //Scripts.ReadStreamForward; + commandText = Scripts.Functions.ReadStreamForward; getNextSequenceNumber = events => events.Last().StreamVersion + 1; } else { - commandText = "read_stream_backward"; //Scripts.ReadStreamBackward; + commandText = Scripts.Functions.ReadStreamBackward; getNextSequenceNumber = events => events.Last().StreamVersion - 1; } - //using (var connection = await this.OpenConnection(cancellationToken)) using (var tx = connection.BeginTransaction()) - using (var command = new NpgsqlCommand(commandText, connection, tx)) + using (var command = new NpgsqlCommand(commandText, connection, tx) { CommandType = CommandType.StoredProcedure }) { - command.CommandType = CommandType.StoredProcedure; - command.Parameters.AddWithValue(":stream_id", streamIdInfo.StreamId); - command.Parameters.AddWithValue(":count", count + 1); //Read extra row to see if at end or not - command.Parameters.AddWithValue(":stream_version", StreamVersion); + command.Parameters.AddWithValue(":stream_id", NpgsqlDbType.Text, streamIdInfo.StreamId); + command.Parameters.AddWithValue(":count", NpgsqlDbType.Integer, count + 1); //Read extra row to see if at end or not + command.Parameters.AddWithValue(":stream_version", NpgsqlDbType.Integer, StreamVersion); List streamEvents = new List(); @@ -395,8 +369,6 @@ public async Task InitializeStore( bool ignoreErrors = false, CancellationToken cancellationToken = default(CancellationToken)) { - - //using (var connection = await this.OpenConnection(cancellationToken)) using(var cmd = new NpgsqlCommand(Scripts.InitializeStore, connection)) { if (ignoreErrors) @@ -416,7 +388,6 @@ public async Task DropAll( bool ignoreErrors = false, CancellationToken cancellationToken = default(CancellationToken)) { - //using (var connection = await this.OpenConnection(cancellationToken)) using(var cmd = new NpgsqlCommand(Scripts.DropAll, connection)) { if (ignoreErrors) @@ -459,13 +430,6 @@ private static StreamIdInfo HashStreamId(string streamId) return new StreamIdInfo(hashedStreamId, streamId); } - private async Task OpenConnection(CancellationToken cancellationToken = default(CancellationToken)) - { - var connection = _createConnection(); - await connection.OpenAsync(cancellationToken); - return connection; - } - private class StreamIdInfo { public readonly string StreamId; diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/BulkCopyEvents.sql b/src/Cedar.EventStore.Postgres/SqlScripts/BulkCopyEvents.sql new file mode 100644 index 000000000..a209a307e --- /dev/null +++ b/src/Cedar.EventStore.Postgres/SqlScripts/BulkCopyEvents.sql @@ -0,0 +1,2 @@ +COPY events (stream_id_internal, stream_version, id, created, type, json_data, json_metadata) +FROM STDIN BINARY \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/CreateStream.sql b/src/Cedar.EventStore.Postgres/SqlScripts/CreateStream.sql index d1b0ac63b..3b706ad01 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/CreateStream.sql +++ b/src/Cedar.EventStore.Postgres/SqlScripts/CreateStream.sql @@ -1,20 +1,3 @@ ---SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; ---BEGIN TRANSACTION CreateStream; --- integer stream_id_internal; --- BEGIN --- INSERT INTO dbo.Streams (Id, IdOriginal) VALUES (@streamId, @streamIdOriginal); --- SELECT @streamIdInternal = SCOPE_IDENTITY(); - --- INSERT INTO dbo.Events (StreamIdInternal, StreamVersion, Id, Created, [Type], JsonData, JsonMetadata) --- SELECT @streamIdInternal, --- StreamVersion, --- Id, --- Created, --- [Type], --- JsonData, --- JsonMetadata --- FROM @events; - --- END; --- SELECT @streamIdInternal; ---COMMIT TRANSACTION CreateStream; +INSERT INTO streams(id, id_original) +VALUES (:stream_id, :stream_id_original) +RETURNING id_internal; \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamAnyVersion.sql b/src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamAnyVersion.sql deleted file mode 100644 index a28e8ba1d..000000000 --- a/src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamAnyVersion.sql +++ /dev/null @@ -1,15 +0,0 @@ -SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -BEGIN TRANSACTION DeleteStream - DECLARE @streamIdInternal AS INT - - SELECT @streamIdInternal = Streams.IdInternal - FROM Streams - WHERE Streams.Id = @streamId; - - DELETE FROM Events - WHERE Events.StreamIdInternal = @streamIdInternal; - - UPDATE Streams - SET IsDeleted = '1' - WHERE Streams.Id = @streamId; -COMMIT TRANSACTION DeleteStream \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamExpectedVersion.sql b/src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamExpectedVersion.sql deleted file mode 100644 index 54ff874d7..000000000 --- a/src/Cedar.EventStore.Postgres/SqlScripts/DeleteStreamExpectedVersion.sql +++ /dev/null @@ -1,37 +0,0 @@ -SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -BEGIN TRANSACTION DeleteStream - DECLARE @streamIdInternal AS INT; - DECLARE @latestStreamVersion AS INT; - - SELECT @streamIdInternal = Streams.IdInternal - FROM Streams - WHERE Streams.Id = @streamId; - - IF @streamIdInternal IS NULL - BEGIN - ROLLBACK TRANSACTION DeleteStream; - RAISERROR('WrongExpectedVersion', 1,1); - RETURN; - END - - SELECT TOP(1) - @latestStreamVersion = Events.StreamVersion - FROM Events - WHERE Events.StreamIDInternal = @streamIdInternal - ORDER BY Events.Ordinal DESC; - - IF @latestStreamVersion != @expectedStreamVersion - BEGIN - ROLLBACK TRANSACTION DeleteStream; - RAISERROR('WrongExpectedVersion', 1,2); - RETURN; - END - - UPDATE Streams - SET IsDeleted = '1' - WHERE Streams.Id = @streamId ; - - DELETE FROM Events - WHERE Events.StreamIdInternal = @streamIdInternal; - -COMMIT TRANSACTION DeleteStream \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/GetStream.sql b/src/Cedar.EventStore.Postgres/SqlScripts/GetStream.sql new file mode 100644 index 000000000..f592b0855 --- /dev/null +++ b/src/Cedar.EventStore.Postgres/SqlScripts/GetStream.sql @@ -0,0 +1,9 @@ +SELECT streams.id_internal, + streams.is_deleted, + events.stream_version +FROM streams +LEFT JOIN events + ON events.stream_id_internal = streams.id_internal +WHERE streams.id = :stream_id +ORDER BY events.ordinal +LIMIT 1; \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/GetVersion.sql b/src/Cedar.EventStore.Postgres/SqlScripts/GetVersion.sql deleted file mode 100644 index ba9e80e5f..000000000 --- a/src/Cedar.EventStore.Postgres/SqlScripts/GetVersion.sql +++ /dev/null @@ -1,3 +0,0 @@ -DECLARE @ver nvarchar(128) -SET @ver = CAST(serverproperty('ProductVersion') AS nvarchar) -SET @ver = SUBSTRING(@ver, 1, CHARINDEX('.', @ver) - 1) \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql index 4eca42613..bc929d020 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql +++ b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql @@ -4,7 +4,9 @@ CREATE TABLE streams( id_original text NOT NULL, is_deleted boolean DEFAULT (false) NOT NULL ); -CREATE UNIQUE INDEX ix_streams_id ON streams USING btree(id); +CREATE UNIQUE INDEX ix_streams_id +ON streams +USING btree(id); CREATE TABLE events( stream_id_internal integer NOT NULL, @@ -18,16 +20,93 @@ CREATE TABLE events( CONSTRAINT fk_events_streams FOREIGN KEY (stream_id_internal) REFERENCES streams(id_internal) ); -CREATE UNIQUE INDEX ix_events_stream_id_internal_revision ON events USING btree(stream_id_internal, stream_version); +CREATE UNIQUE INDEX ix_events_stream_id_internal_revision +ON events +USING btree(stream_id_internal, stream_version); + +CREATE OR REPLACE FUNCTION create_stream(_stream_id text, _stream_id_original text) +RETURNS integer AS +$BODY$ +DECLARE + _result integer; +BEGIN + INSERT INTO streams(id, id_original) + VALUES (_stream_id, _stream_id_original) + RETURNING id_internal + INTO _result; + + RETURN _result; +END; +$BODY$ +LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION get_stream(_stream_id text) +RETURNS TABLE(id_internal integer, is_deleted boolean, stream_version integer) AS +$BODY$ +BEGIN + RETURN QUERY + SELECT streams.id_internal, + streams.is_deleted, + events.stream_version + FROM streams + LEFT JOIN events + ON events.stream_id_internal = streams.id_internal + WHERE streams.id = _stream_id + ORDER BY events.ordinal + LIMIT 1; +END; +$BODY$ +LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION read_all_forward(_ordinal bigint, _count integer) +RETURNS TABLE(stream_id integer, stream_version integer, ordinal bigint, event_id uuid, created timestamp, type text, json_data json, json_metadata json) AS +$BODY$ +BEGIN + RETURN QUERY + SELECT + streams.id_original As stream_id, + events.stream_version, + events.ordinal, + events.id AS event_id, + events.created, + events.type, + events.json_data, + events.json_metadata + FROM events + INNER JOIN streams + ON events.stream_id_internal = streams.id_internal + WHERE events.ordinal >= _ordinal + ORDER BY events.ordinal + LIMIT _count; +END; +$BODY$ +LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION read_all_backward(_ordinal bigint, _count integer) +RETURNS TABLE(stream_id integer, stream_version integer, ordinal bigint, event_id uuid, created timestamp, type text, json_data json, json_metadata json) AS +$BODY$ +BEGIN + RETURN QUERY + SELECT + streams.id_original As stream_id, + events.stream_version, + events.ordinal, + events.id AS event_id, + events.created, + events.type, + events.json_data, + events.json_metadata + FROM events + INNER JOIN streams + ON events.stream_id_internal = streams.id_internal + WHERE events.ordinal <= _ordinal + ORDER BY events.ordinal DESC + LIMIT _count; +END; +$BODY$ +LANGUAGE plpgsql; -CREATE TYPE new_stream_events AS ( - stream_version integer, - id uuid, - created timestamp, - type text, - json_data json, - json_metadata json -); CREATE OR REPLACE FUNCTION read_stream_forward(_stream_id text, _count integer, _stream_version integer) RETURNS SETOF refcursor AS $BODY$ @@ -38,7 +117,6 @@ DECLARE _stream_id_internal integer; _is_deleted boolean; BEGIN - SELECT streams.id_internal, streams.is_deleted INTO _stream_id_internal, _is_deleted FROM streams @@ -169,7 +247,8 @@ BEGIN WHERE streams.id = stream_id; IF _stream_id_internal IS NULL THEN - RAISE EXCEPTION 'WrongExpectedVersion'; + RAISE EXCEPTION 'WrongExpectedVersion' + USING HINT = 'The Stream ' || stream_id || ' does not exist.'; END IF; SELECT stream_version @@ -180,7 +259,8 @@ BEGIN LIMIT 1; IF (_lastest_stream_version <> expected_version) THEN - RAISE EXCEPTION 'WrongExpectedVersion'; + RAISE EXCEPTION 'WrongExpectedVersion' + USING HINT = 'The Stream ' || stream_id || 'version was expected to be' || expected_version::text || ' but was version ' || _lastest_stream_version::text || '.' ; END IF; DELETE FROM events diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamBackward.sql b/src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamBackward.sql deleted file mode 100644 index 17898f621..000000000 --- a/src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamBackward.sql +++ /dev/null @@ -1,31 +0,0 @@ -/* SQL Server 2008+ */ - - DECLARE @streamIdInternal AS INT - DECLARE @isDeleted AS BIT - - SELECT @streamIdInternal = Streams.IdInternal, - @isDeleted = Streams.IsDeleted - FROM Streams - WHERE Streams.Id = @streamId - - SELECT @isDeleted; - - SELECT TOP(@count) - Events.StreamVersion, - Events.Ordinal, - Events.Id AS EventId, - Events.Created, - Events.Type, - Events.JsonData, - Events.JsonMetadata - FROM Events - INNER JOIN Streams - ON Events.StreamIdInternal = Streams.IdInternal - WHERE Events.StreamIDInternal = @streamIDInternal AND Events.StreamVersion <= @StreamVersion - ORDER BY Events.Ordinal DESC - - SELECT TOP(1) - Events.StreamVersion - FROM Events - WHERE Events.StreamIDInternal = @streamIDInternal - ORDER BY Events.Ordinal DESC; \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamForward.sql b/src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamForward.sql deleted file mode 100644 index f7fd32d78..000000000 --- a/src/Cedar.EventStore.Postgres/SqlScripts/ReadStreamForward.sql +++ /dev/null @@ -1,32 +0,0 @@ -/* SQL Server 2008+ */ - - DECLARE @streamIdInternal AS INT - DECLARE @isDeleted AS BIT - - SELECT @streamIdInternal = Streams.IdInternal, - @isDeleted = Streams.IsDeleted - FROM Streams - WHERE Streams.Id = @streamId - - SELECT @isDeleted; - - SELECT - events.stream_version, - events.ordinal, - events.id AS event_id, - events.created, - events.type, - events.json_data, - events.json_metadata - FROM events - INNER JOIN streams - ON events.stream_id_internal = streams.id_internal - WHERE events.stream_id_internal = :stream_id_internal AND events.stream_version >= :stream_version - ORDER BY events.Ordinal - LIMIT :count; - - SELECT events.StreamVersion - FROM events - WHERE events.StreamIDInternal = @streamIDInternal - ORDER BY events.Ordinal DESC - LIMIT 1; \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/Scripts.cs b/src/Cedar.EventStore.Postgres/SqlScripts/Scripts.cs index c0e3b397d..f6cc3d715 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/Scripts.cs +++ b/src/Cedar.EventStore.Postgres/SqlScripts/Scripts.cs @@ -19,19 +19,9 @@ public static string DropAll get { return GetScript("DropAll"); } } - public static string CreateStream + public static string BulkCopyEvents { - get { return GetScript("CreateStream"); } - } - - public static string DeleteStreamAnyVersion - { - get { return GetScript("DeleteStreamAnyVersion"); } - } - - public static string DeleteStreamExpectedVersion - { - get { return GetScript("DeleteStreamExpectedVersion"); } + get { return GetScript("BulkCopyEvents"); } } public static string ReadAllForward @@ -44,16 +34,6 @@ public static string ReadAllBackward get { return GetScript("ReadAllBackward"); } } - public static string ReadStreamForward - { - get { return GetScript("ReadStreamForward"); } - } - - public static string ReadStreamBackward - { - get { return GetScript("ReadStreamBackward"); } - } - private static string GetScript(string name) { return s_scripts.GetOrAdd(name, @@ -74,5 +54,48 @@ private static string GetScript(string name) } }); } + + public static class Functions + { + public static string CreateStream + { + get { return "create_stream"; } + } + + public static string GetStream + { + get { return "get_stream"; } + } + + public static string ReadAllForward + { + get { return "read_all_forward"; } + } + + public static string ReadAllBackward + { + get { return "read_all_backward"; } + } + + public static string ReadStreamForward + { + get { return "read_stream_forward"; } + } + + public static string ReadStreamBackward + { + get { return "read_stream_backward"; } + } + + public static string DeleteStreamAnyVersion + { + get { return "delete_stream_any_version"; } + } + + public static string DeleteStreamExpectedVersion + { + get { return "delete_stream_expected_version"; } + } + } } } \ No newline at end of file From aca368fe2db2d6adb872c70f75a7f173517fbef0 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Fri, 19 Jun 2015 16:35:51 +0100 Subject: [PATCH 06/34] Fix release build --- src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj index 4603e832e..0eb9c4dec 100644 --- a/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj +++ b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj @@ -28,6 +28,7 @@ TRACE prompt 4 + bin\Release\Cedar.EventStore.Postgres.XML From 5fd46f6a50c1f993b7e6b73549b350b6d3d37763 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Mon, 22 Jun 2015 09:29:07 +0100 Subject: [PATCH 07/34] Fix build errors after merge --- .../PostgresEventStoreTests.cs | 6 ++++++ src/Cedar.EventStore.Postgres/PostgresEventStore.cs | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreTests.cs b/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreTests.cs index 652c465e1..75bd38634 100644 --- a/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreTests.cs +++ b/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreTests.cs @@ -1,7 +1,13 @@ namespace Cedar.EventStore.Postgres.Tests { + using Xunit.Abstractions; + public class PostgresEventStoreTests : EventStoreAcceptanceTests { + public PostgresEventStoreTests(ITestOutputHelper testOutputHelper) + : base(testOutputHelper) + {} + protected override EventStoreAcceptanceTestFixture GetFixture() { return new PostgresEventStoreFixture(); diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs index f85bd9ea5..0f557af14 100644 --- a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -90,7 +90,8 @@ public async Task AppendToStream( if(expectedVersion != ExpectedVersion.Any && currentVersion != expectedVersion) { - throw new WrongExpectedVersionException(streamId, expectedVersion); + throw new WrongExpectedVersionException( + Messages.AppendFailedWrongExpectedVersion.FormatWith(streamId, expectedVersion)); } } @@ -171,7 +172,8 @@ await command { if(ex.MessageText == "WrongExpectedVersion") { - throw new WrongExpectedVersionException(streamId, expectedVersion, ex); + throw new WrongExpectedVersionException( + Messages.AppendFailedWrongExpectedVersion.FormatWith(streamId, expectedVersion), ex); } throw; } From 625e5866a936d6f576a112b3f50b6fa158ab63e2 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Mon, 22 Jun 2015 09:41:24 +0100 Subject: [PATCH 08/34] Use correct ordering when getting stream head --- src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql index bc929d020..2987cf808 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql +++ b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql @@ -53,7 +53,7 @@ BEGIN LEFT JOIN events ON events.stream_id_internal = streams.id_internal WHERE streams.id = _stream_id - ORDER BY events.ordinal + ORDER BY events.ordinal DESC LIMIT 1; END; $BODY$ From 6235b7085fdfbfdb565900c6cac615cb9e18f99d Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Mon, 22 Jun 2015 09:41:41 +0100 Subject: [PATCH 09/34] Fix merge --- .../EventStoreAcceptanceTests.DeleteStream.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.DeleteStream.cs b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.DeleteStream.cs index d51079fa0..cfc4b542b 100644 --- a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.DeleteStream.cs +++ b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.DeleteStream.cs @@ -83,6 +83,8 @@ public async Task When_delete_a_stream_and_append_then_should_throw() const string streamId = "stream"; await eventStore.AppendToStream(streamId, ExpectedVersion.NoStream, CreateNewStreamEvents(1)); await eventStore.DeleteStream(streamId); + + var events = new[]{ new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\""), new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\"") }; @@ -105,7 +107,7 @@ public async Task When_delete_stream_that_does_not_exist_with_expected_version_t { const string streamId = "notexist"; const int expectedVersion = 1; - const string streamId = "stream"; + var events = new[] { new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\""), @@ -131,8 +133,8 @@ public async Task When_delete_stream_with_a_non_matching_expected_version_then_s const string streamId = "stream"; var events = new[] { - new NewStreamEvent(Guid.NewGuid(), "data", "meta"), - new NewStreamEvent(Guid.NewGuid(), "data", "meta") + new NewStreamEvent(Guid.NewGuid(), "\"data\"", "\"meta\""), + new NewStreamEvent(Guid.NewGuid(), "\"data\"", "\"meta\"") }; await eventStore.AppendToStream(streamId, ExpectedVersion.NoStream, events); From 1fe4a4730e28947b8b55ffc4f87658a04615753a Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Mon, 22 Jun 2015 10:04:59 +0100 Subject: [PATCH 10/34] Use json compatible stub data --- .../EventStoreAcceptanceTests.DeleteStream.cs | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.DeleteStream.cs b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.DeleteStream.cs index cfc4b542b..f61fb83f0 100644 --- a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.DeleteStream.cs +++ b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.DeleteStream.cs @@ -11,9 +11,9 @@ public abstract partial class EventStoreAcceptanceTests [Fact] public async Task When_delete_existing_stream_with_no_expected_version_then_should_be_deleted() { - using(var fixture = GetFixture()) + using (var fixture = GetFixture()) { - using(var eventStore = await fixture.GetEventStore()) + using (var eventStore = await fixture.GetEventStore()) { const string streamId = "stream"; var events = new[] @@ -81,16 +81,10 @@ public async Task When_delete_a_stream_and_append_then_should_throw() using (var eventStore = await fixture.GetEventStore()) { const string streamId = "stream"; + await eventStore.AppendToStream(streamId, ExpectedVersion.NoStream, CreateNewStreamEvents(1)); await eventStore.DeleteStream(streamId); - var events = new[]{ - new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\""), - new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\"") - }; - - await eventStore.AppendToStream(streamId, ExpectedVersion.NoStream, events); - await eventStore .AppendToStream(streamId, ExpectedVersion.Any, CreateNewStreamEvents(2)) .ShouldThrow(Messages.EventStreamIsDeleted.FormatWith(streamId)); @@ -108,14 +102,6 @@ public async Task When_delete_stream_that_does_not_exist_with_expected_version_t const string streamId = "notexist"; const int expectedVersion = 1; - var events = new[] - { - new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\""), - new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\"") - }; - - await eventStore.AppendToStream(streamId, ExpectedVersion.NoStream, events); - await eventStore.DeleteStream(streamId, 1) .ShouldThrow( Messages.DeleteStreamFailedWrongExpectedVersion.FormatWith(streamId, expectedVersion)); @@ -133,8 +119,8 @@ public async Task When_delete_stream_with_a_non_matching_expected_version_then_s const string streamId = "stream"; var events = new[] { - new NewStreamEvent(Guid.NewGuid(), "\"data\"", "\"meta\""), - new NewStreamEvent(Guid.NewGuid(), "\"data\"", "\"meta\"") + new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\""), + new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"headers\"") }; await eventStore.AppendToStream(streamId, ExpectedVersion.NoStream, events); From d6d344d0ba22c4d0219c4a8b8d1b790511e5022a Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Mon, 22 Jun 2015 10:05:14 +0100 Subject: [PATCH 11/34] Make new pgsql tests pass --- .../PostgresEventStore.cs | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs index 0f557af14..efec963fb 100644 --- a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -54,13 +54,35 @@ public async Task AppendToStream( if(expectedVersion == ExpectedVersion.NoStream) { - using(var command = new NpgsqlCommand(Scripts.Functions.CreateStream, connection, tx){ CommandType = CommandType.StoredProcedure}) + try { - command.Parameters.AddWithValue(":stream_id", streamIdInfo.StreamId); - command.Parameters.AddWithValue(":stream_id_original", streamIdInfo.StreamIdOriginal); + using( + var command = new NpgsqlCommand(Scripts.Functions.CreateStream, connection, tx) + { + CommandType + = + CommandType + .StoredProcedure + }) + { + command.Parameters.AddWithValue(":stream_id", streamIdInfo.StreamId); + command.Parameters.AddWithValue(":stream_id_original", streamIdInfo.StreamIdOriginal); - streamIdInternal = - (int)await command.ExecuteScalarAsync(cancellationToken).NotOnCapturedContext(); + streamIdInternal = + (int)await command.ExecuteScalarAsync(cancellationToken).NotOnCapturedContext(); + } + } + catch(NpgsqlException ex) + { + tx.Rollback(); + + if(ex.Code == "23505") + { + throw new WrongExpectedVersionException( + Messages.AppendFailedWrongExpectedVersion.FormatWith(streamId, expectedVersion)); + } + + throw; } } else @@ -85,7 +107,7 @@ public async Task AppendToStream( if(isDeleted) { - throw new StreamDeletedException(streamId); + throw new StreamDeletedException(Messages.EventStreamIsDeleted.FormatWith(streamId)); } if(expectedVersion != ExpectedVersion.Any && currentVersion != expectedVersion) @@ -135,17 +157,17 @@ public Task DeleteStream( var streamIdInfo = HashStreamId(streamId); return expectedVersion == ExpectedVersion.Any - ? this.DeleteStreamAnyVersion(streamIdInfo.StreamId, cancellationToken) - : this.DeleteStreamExpectedVersion(streamIdInfo.StreamId, expectedVersion, cancellationToken); + ? this.DeleteStreamAnyVersion(streamIdInfo, cancellationToken) + : this.DeleteStreamExpectedVersion(streamIdInfo, expectedVersion, cancellationToken); } private async Task DeleteStreamAnyVersion( - string streamId, + StreamIdInfo streamIdInfo, CancellationToken cancellationToken) { using (var command = new NpgsqlCommand(Scripts.Functions.DeleteStreamAnyVersion, connection) { CommandType = CommandType.StoredProcedure }) { - command.Parameters.AddWithValue("stream_id", streamId); + command.Parameters.AddWithValue("stream_id", streamIdInfo.StreamId); await command .ExecuteNonQueryAsync(cancellationToken) .NotOnCapturedContext(); @@ -154,13 +176,13 @@ await command private async Task DeleteStreamExpectedVersion( - string streamId, + StreamIdInfo streamIdInfo, int expectedVersion, CancellationToken cancellationToken) { using (var command = new NpgsqlCommand(Scripts.Functions.DeleteStreamExpectedVersion, connection) { CommandType = CommandType.StoredProcedure }) { - command.Parameters.AddWithValue("stream_id", streamId); + command.Parameters.AddWithValue("stream_id", streamIdInfo.StreamId); command.Parameters.AddWithValue("expected_version", expectedVersion); try { @@ -172,8 +194,7 @@ await command { if(ex.MessageText == "WrongExpectedVersion") { - throw new WrongExpectedVersionException( - Messages.AppendFailedWrongExpectedVersion.FormatWith(streamId, expectedVersion), ex); + throw new WrongExpectedVersionException(Messages.DeleteStreamFailedWrongExpectedVersion.FormatWith(streamIdInfo.StreamIdOriginal, expectedVersion)); } throw; } From 8305a184b52d1d9d3aeddb90808c110b73e93b9f Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Mon, 22 Jun 2015 10:07:01 +0100 Subject: [PATCH 12/34] add .ncrunchproject files --- ...tore.GetEventStore.Tests.v2.ncrunchproject | Bin 3212 -> 3030 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/Cedar.EventStore.GetEventStore.Tests/Cedar.EventStore.GetEventStore.Tests.v2.ncrunchproject b/src/Cedar.EventStore.GetEventStore.Tests/Cedar.EventStore.GetEventStore.Tests.v2.ncrunchproject index 9862e303478350c6c5bff7bd9950ae3f3ce5cd51..8782fa034e619e9f43e86fcb6dd356a536eaf6c8 100644 GIT binary patch delta 20 bcmeB?ye7VZg_Bu JpS+OU6aWI;An*VH From b0e587a0ca22d4b82c2aa29c42f107b6adb05de1 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Mon, 22 Jun 2015 10:17:55 +0100 Subject: [PATCH 13/34] Add nuget.config to see if we can pull in npgsql from myget --- src/nuget.config | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/nuget.config diff --git a/src/nuget.config b/src/nuget.config new file mode 100644 index 000000000..aa433be65 --- /dev/null +++ b/src/nuget.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From 15cc2bb7675bb40f2c3ebe91976cbd5f1ab75d07 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Thu, 16 Jul 2015 11:15:04 +0100 Subject: [PATCH 14/34] Change PostgresEventStore ctor to take a connection string or connection string name (can't leak Npgsql types due to assembly versioning) Change connection handling to create connection on-demand (issues with TPL + concurrency it seems) - lean on Npgsql connection pooling instead --- .../PostgresEventStoreFixture.cs | 8 +--- .../Cedar.EventStore.Postgres.csproj | 1 + .../PostgresEventStore.cs | 39 ++++++++++++++----- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreFixture.cs b/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreFixture.cs index 576c1de07..aec05c1ee 100644 --- a/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreFixture.cs +++ b/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreFixture.cs @@ -1,18 +1,12 @@ namespace Cedar.EventStore.Postgres.Tests { - using System; using System.Threading.Tasks; - using Npgsql; - public class PostgresEventStoreFixture : EventStoreAcceptanceTestFixture { public override async Task GetEventStore() { - Func createConnectionFunc = () => new NpgsqlConnection( - @"Server=127.0.0.1;Port=5432;Database=cedar_tests;User Id=postgres;Password=postgres;"); - - var eventStore = new PostgresEventStore(createConnectionFunc); + var eventStore = new PostgresEventStore(@"Server=127.0.0.1;Port=5432;Database=cedar_tests;User Id=postgres;Password=postgres;"); await eventStore.DropAll(ignoreErrors: true); await eventStore.InitializeStore(); diff --git a/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj index 0eb9c4dec..5615b72e5 100644 --- a/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj +++ b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj @@ -40,6 +40,7 @@ True + diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs index efec963fb..273ff0239 100644 --- a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Configuration; using System.Data; using System.Linq; using System.Security.Cryptography; @@ -20,18 +21,31 @@ public class PostgresEventStore : IEventStore { - private readonly Func _createConnection; + private readonly Func> _createAndOpenConnection; - private readonly NpgsqlConnection connection; private readonly InterlockedBoolean _isDisposed = new InterlockedBoolean(); - public PostgresEventStore(Func createConnection) + public PostgresEventStore(string connectionStringOrConnectionStringName) { - Ensure.That(createConnection, "createConnection").IsNotNull(); - - _createConnection = createConnection; - connection = createConnection(); - connection.Open(); + if(connectionStringOrConnectionStringName.IndexOf(';') > -1) + { + var builder = new NpgsqlConnectionStringBuilder(connectionStringOrConnectionStringName); + _createAndOpenConnection = async () => + { + var connection = new NpgsqlConnection(builder); + await connection.OpenAsync(); + return connection; + }; + } + else + { + _createAndOpenConnection = async () => + { + var connection = new NpgsqlConnection(ConfigurationManager.ConnectionStrings[connectionStringOrConnectionStringName].ConnectionString); + await connection.OpenAsync(); + return connection; + }; + } } public async Task AppendToStream( @@ -46,6 +60,7 @@ public async Task AppendToStream( var streamIdInfo = HashStreamId(streamId); + using(var connection = await _createAndOpenConnection()) using(var tx = connection.BeginTransaction(IsolationLevel.Serializable)) { int streamIdInternal = -1; @@ -165,6 +180,7 @@ private async Task DeleteStreamAnyVersion( StreamIdInfo streamIdInfo, CancellationToken cancellationToken) { + using (var connection = await _createAndOpenConnection()) using (var command = new NpgsqlCommand(Scripts.Functions.DeleteStreamAnyVersion, connection) { CommandType = CommandType.StoredProcedure }) { command.Parameters.AddWithValue("stream_id", streamIdInfo.StreamId); @@ -180,6 +196,7 @@ private async Task DeleteStreamExpectedVersion( int expectedVersion, CancellationToken cancellationToken) { + using (var connection = await _createAndOpenConnection()) using (var command = new NpgsqlCommand(Scripts.Functions.DeleteStreamExpectedVersion, connection) { CommandType = CommandType.StoredProcedure }) { command.Parameters.AddWithValue("stream_id", streamIdInfo.StreamId); @@ -219,6 +236,7 @@ public async Task ReadAll( var commandText = direction == ReadDirection.Forward ? Scripts.ReadAllForward : Scripts.ReadAllBackward; + using (var connection = await _createAndOpenConnection()) using (var command = new NpgsqlCommand(commandText, connection))// { CommandType = CommandType.StoredProcedure }) { command.Parameters.AddWithValue(":ordinal", NpgsqlDbType.Bigint, ordinal); @@ -306,6 +324,7 @@ public async Task ReadStream( getNextSequenceNumber = events => events.Last().StreamVersion - 1; } + using (var connection = await _createAndOpenConnection()) using (var tx = connection.BeginTransaction()) using (var command = new NpgsqlCommand(commandText, connection, tx) { CommandType = CommandType.StoredProcedure }) { @@ -385,13 +404,14 @@ public void Dispose() { return; } - this.connection.Dispose(); + //no clean up to do, lean on Npgsql connection pooling } public async Task InitializeStore( bool ignoreErrors = false, CancellationToken cancellationToken = default(CancellationToken)) { + using (var connection = await _createAndOpenConnection()) using(var cmd = new NpgsqlCommand(Scripts.InitializeStore, connection)) { if (ignoreErrors) @@ -411,6 +431,7 @@ public async Task DropAll( bool ignoreErrors = false, CancellationToken cancellationToken = default(CancellationToken)) { + using (var connection = await _createAndOpenConnection()) using(var cmd = new NpgsqlCommand(Scripts.DropAll, connection)) { if (ignoreErrors) From 3af1d8163e620b3b145166f4fb57046110a7ba7c Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Fri, 17 Jul 2015 11:56:59 +0100 Subject: [PATCH 15/34] Correct ordering for GetStream.sql --- src/Cedar.EventStore.Postgres/SqlScripts/GetStream.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/GetStream.sql b/src/Cedar.EventStore.Postgres/SqlScripts/GetStream.sql index f592b0855..c91287746 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/GetStream.sql +++ b/src/Cedar.EventStore.Postgres/SqlScripts/GetStream.sql @@ -5,5 +5,5 @@ FROM streams LEFT JOIN events ON events.stream_id_internal = streams.id_internal WHERE streams.id = :stream_id -ORDER BY events.ordinal +ORDER BY events.ordinal DESC LIMIT 1; \ No newline at end of file From 6550a81ecd60fb7609c926a8ce987602dc14a66d Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Fri, 17 Jul 2015 12:06:36 +0100 Subject: [PATCH 16/34] Include ordinal in ix_events_stream_id_internal_revision --- src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql index 2987cf808..fe9c2bd2c 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql +++ b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql @@ -22,7 +22,7 @@ CREATE TABLE events( CREATE UNIQUE INDEX ix_events_stream_id_internal_revision ON events -USING btree(stream_id_internal, stream_version); +USING btree(stream_id_internal, ordinal, stream_version); CREATE OR REPLACE FUNCTION create_stream(_stream_id text, _stream_id_original text) RETURNS integer AS From 747973d7aafd1a8ee664c015f8d8d9a8e0c258d7 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Fri, 17 Jul 2015 13:14:00 +0100 Subject: [PATCH 17/34] Optimization: When expected version passed in, filter for events > expected_version to get current version. If current_version is null then it's < expected version, if > expected_version then we don't have as many records to sort in order to find MAX(current_version) If using ExpectedVersion.Any then it will have to sort through all of the events. --- src/Cedar.EventStore.Postgres/PostgresEventStore.cs | 12 +++++++++++- .../SqlScripts/InitializeStore.sql | 6 +++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs index 273ff0239..6a78fe068 100644 --- a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -105,6 +105,7 @@ public async Task AppendToStream( using (var command = new NpgsqlCommand(Scripts.Functions.GetStream, connection, tx) { CommandType = CommandType.StoredProcedure }) { command.Parameters.AddWithValue(":stream_id", streamIdInfo.StreamId); + command.Parameters.AddWithValue(":expected_version", expectedVersion); using(var dr = await command.ExecuteReaderAsync().NotOnCapturedContext()) { @@ -112,8 +113,17 @@ public async Task AppendToStream( { streamIdInternal = dr.GetInt32(0); isDeleted = dr.GetBoolean(1); - if(!isDeleted) + + if (!isDeleted) { + if (expectedVersion != ExpectedVersion.Any && dr.IsDBNull(2)) + { + /* optimisation: getting the stream revision on a large stream is costly. + * we're joining on (events.stream_version >= _expected_version OR _expected_version < 0) + * if current version is null then it's less than the expected version and we can short circuit */ + throw new WrongExpectedVersionException(Messages.AppendFailedWrongExpectedVersion.FormatWith(streamId, expectedVersion)); + } + currentVersion = dr.GetInt32(2); } } diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql index fe9c2bd2c..57ac95f18 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql +++ b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql @@ -22,7 +22,7 @@ CREATE TABLE events( CREATE UNIQUE INDEX ix_events_stream_id_internal_revision ON events -USING btree(stream_id_internal, ordinal, stream_version); +USING btree(stream_id_internal, ordinal DESC, stream_version); CREATE OR REPLACE FUNCTION create_stream(_stream_id text, _stream_id_original text) RETURNS integer AS @@ -40,8 +40,7 @@ END; $BODY$ LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION get_stream(_stream_id text) +CREATE OR REPLACE FUNCTION get_stream(_stream_id text, _expected_version integer) RETURNS TABLE(id_internal integer, is_deleted boolean, stream_version integer) AS $BODY$ BEGIN @@ -52,6 +51,7 @@ BEGIN FROM streams LEFT JOIN events ON events.stream_id_internal = streams.id_internal + AND (events.stream_version >= _expected_version OR _expected_version < 0) WHERE streams.id = _stream_id ORDER BY events.ordinal DESC LIMIT 1; From c491bc3f045e1e53ed4c01a6f4fdac483c2e27ba Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Mon, 20 Jul 2015 18:10:02 +0100 Subject: [PATCH 18/34] Optimize get_stream, subquery uses index more efficiently than join + limit. ExpectedVersion.Any should now perform as well as a given expected version. --- .../PostgresEventStore.cs | 11 +---------- .../SqlScripts/InitializeStore.sql | 14 +++++--------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs index 6a78fe068..82214e09f 100644 --- a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -105,7 +105,6 @@ public async Task AppendToStream( using (var command = new NpgsqlCommand(Scripts.Functions.GetStream, connection, tx) { CommandType = CommandType.StoredProcedure }) { command.Parameters.AddWithValue(":stream_id", streamIdInfo.StreamId); - command.Parameters.AddWithValue(":expected_version", expectedVersion); using(var dr = await command.ExecuteReaderAsync().NotOnCapturedContext()) { @@ -113,17 +112,9 @@ public async Task AppendToStream( { streamIdInternal = dr.GetInt32(0); isDeleted = dr.GetBoolean(1); - + if (!isDeleted) { - if (expectedVersion != ExpectedVersion.Any && dr.IsDBNull(2)) - { - /* optimisation: getting the stream revision on a large stream is costly. - * we're joining on (events.stream_version >= _expected_version OR _expected_version < 0) - * if current version is null then it's less than the expected version and we can short circuit */ - throw new WrongExpectedVersionException(Messages.AppendFailedWrongExpectedVersion.FormatWith(streamId, expectedVersion)); - } - currentVersion = dr.GetInt32(2); } } diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql index 57ac95f18..9046c8c38 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql +++ b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql @@ -4,6 +4,7 @@ CREATE TABLE streams( id_original text NOT NULL, is_deleted boolean DEFAULT (false) NOT NULL ); + CREATE UNIQUE INDEX ix_streams_id ON streams USING btree(id); @@ -22,7 +23,7 @@ CREATE TABLE events( CREATE UNIQUE INDEX ix_events_stream_id_internal_revision ON events -USING btree(stream_id_internal, ordinal DESC, stream_version); +USING btree(stream_id_internal, stream_version DESC, ordinal DESC); CREATE OR REPLACE FUNCTION create_stream(_stream_id text, _stream_id_original text) RETURNS integer AS @@ -40,21 +41,16 @@ END; $BODY$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION get_stream(_stream_id text, _expected_version integer) +CREATE OR REPLACE FUNCTION get_stream(_stream_id text) RETURNS TABLE(id_internal integer, is_deleted boolean, stream_version integer) AS $BODY$ BEGIN RETURN QUERY SELECT streams.id_internal, streams.is_deleted, - events.stream_version + (SELECT max(events.stream_version) from events where events.stream_id_internal = streams.id_internal) FROM streams - LEFT JOIN events - ON events.stream_id_internal = streams.id_internal - AND (events.stream_version >= _expected_version OR _expected_version < 0) - WHERE streams.id = _stream_id - ORDER BY events.ordinal DESC - LIMIT 1; + WHERE streams.id = _stream_id; END; $BODY$ LANGUAGE plpgsql; From be47314bbbc2d073b26fececd4a9296de8c7475a Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Tue, 21 Jul 2015 16:45:06 +0100 Subject: [PATCH 19/34] Test coverage for non-existent stream read scenarios --- .../EventStoreAcceptanceTests.ReadStream.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadStream.cs b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadStream.cs index 8cdde47aa..c68bc733c 100644 --- a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadStream.cs +++ b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadStream.cs @@ -46,6 +46,42 @@ public async Task Can_read_streams_forwards_and_backwards(ReadStreamTheory theor } } + [Theory] + [InlineData(ReadDirection.Forward, 0, 10)] + [InlineData(ReadDirection.Backward, StreamPosition.End, 10)] + public async Task Empty_Streams_return_StreamNotFound(ReadDirection direction, int start, int pageSize) + { + using(var fixture = GetFixture()) + { + using(var eventStore = await fixture.GetEventStore()) + { + var streamEventsPage = + await eventStore.ReadStream("stream-does-not-exist", start, pageSize, direction); + + streamEventsPage.Status.Should().Be(PageReadStatus.StreamNotFound); + } + } + } + + [Theory] + [InlineData(ReadDirection.Forward, 0, 10)] + [InlineData(ReadDirection.Backward, StreamPosition.End, 10)] + public async Task Deleted_Streams_return_StreamDeleted(ReadDirection direction, int start, int pageSize) + { + using (var fixture = GetFixture()) + { + using (var eventStore = await fixture.GetEventStore()) + { + await eventStore.AppendToStream("stream-1", ExpectedVersion.NoStream, CreateNewStreamEvents(1, 2, 3)); + await eventStore.DeleteStream("stream-1", ExpectedVersion.Any); + + var streamEventsPage = + await eventStore.ReadStream("stream-1", start, pageSize, direction); + + streamEventsPage.Status.Should().Be(PageReadStatus.StreamDeleted); + } + } + } public static IEnumerable GetReadStreamTheories() { var theories = new[] From 9c3df31cc8a45f5ae873f6a07b02c8fd44438462 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Tue, 28 Jul 2015 09:29:35 +0100 Subject: [PATCH 20/34] Add failing test and implemenation in Postgres for end-of-stream checkpoints --- .../PostgresEventStore.cs | 23 +++++++---- .../EventStoreAcceptanceTests.ReadAll.cs | 38 +++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs index 82214e09f..db5b7d957 100644 --- a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -115,7 +115,7 @@ public async Task AppendToStream( if (!isDeleted) { - currentVersion = dr.GetInt32(2); + currentVersion = dr.IsDBNull(2) ? -1 : dr.GetInt32(2); } } } @@ -245,17 +245,20 @@ public async Task ReadAll( List streamEvents = new List(); + long latestCheckpoint = ordinal; + using(var reader = await command.ExecuteReaderAsync(cancellationToken).NotOnCapturedContext()) { if (!reader.HasRows) { return new AllEventsPage(checkpoint.Value, - null, + checkpoint.Value, true, direction, streamEvents.ToArray()); } + while (await reader.ReadAsync(cancellationToken).NotOnCapturedContext()) { var streamId = reader.GetString(0); @@ -267,6 +270,8 @@ public async Task ReadAll( var jsonData = reader.GetString(6); var jsonMetadata = reader.GetString(7); + latestCheckpoint = ordinal; + var streamEvent = new StreamEvent(streamId, eventId, StreamVersion, @@ -281,14 +286,18 @@ public async Task ReadAll( } bool isEnd = true; - string nextCheckpoint = null; + string nextCheckpoint = latestCheckpoint.ToString(); if(streamEvents.Count == maxCount + 1) { isEnd = false; - nextCheckpoint = streamEvents[maxCount].Checkpoint; streamEvents.RemoveAt(maxCount); } + else + { + nextCheckpoint = (latestCheckpoint + 1).ToString(); + } + return new AllEventsPage(checkpoint.Value, nextCheckpoint, @@ -317,12 +326,12 @@ public async Task ReadStream( if(direction == ReadDirection.Forward) { commandText = Scripts.Functions.ReadStreamForward; - getNextSequenceNumber = events => events.Last().StreamVersion + 1; + getNextSequenceNumber = events => events.Count > 0 ? events.Last().StreamVersion + 1 : -1; //todo: review this } else { commandText = Scripts.Functions.ReadStreamBackward; - getNextSequenceNumber = events => events.Last().StreamVersion - 1; + getNextSequenceNumber = events => events.Count > 0 ? events.Last().StreamVersion - 1 : -1; //todo: review this } using (var connection = await _createAndOpenConnection()) @@ -376,7 +385,7 @@ public async Task ReadStream( // Read last event revision result set await reader.NextResultAsync(cancellationToken).NotOnCapturedContext(); await reader.ReadAsync(cancellationToken).NotOnCapturedContext(); - var lastStreamVersion = reader.GetInt32(0); + var lastStreamVersion = reader.HasRows ? reader.GetInt32(0) : -1; //todo: figure out wtf going on here bool isEnd = true; diff --git a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadAll.cs b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadAll.cs index c112fb865..6679bfa01 100644 --- a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadAll.cs +++ b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadAll.cs @@ -52,6 +52,44 @@ public async Task Can_read_all_forwards() } } + [Fact] + public async Task Read_forwards_to_the_end_should_return_a_valid_Checkpoint() + { + using (var fixture = GetFixture()) + { + using (var eventStore = await fixture.GetEventStore()) + { + await eventStore.AppendToStream("stream-1", ExpectedVersion.NoStream, CreateNewStreamEvents(1, 2, 3, 4, 5, 6)); + + // read to the end of the stream + var allEventsPage = await eventStore.ReadAll(Checkpoint.Start, 4); + while (!allEventsPage.IsEnd) + { + allEventsPage = await eventStore.ReadAll(allEventsPage.NextCheckpoint, 10); + } + + allEventsPage.IsEnd.Should().BeTrue(); + + Checkpoint currentCheckpoint = allEventsPage.NextCheckpoint; + currentCheckpoint.Should().NotBeNull(); + + // read end of stream again, should be empty, should return same checkpoint + allEventsPage = await eventStore.ReadAll(currentCheckpoint, 10); + allEventsPage.StreamEvents.Should().BeEmpty(); + allEventsPage.IsEnd.Should().BeTrue(); + allEventsPage.NextCheckpoint.Should().NotBeNull(); + allEventsPage.NextCheckpoint.Should().Be(currentCheckpoint.Value); + + // append some events then read again from the saved checkpoint, the next checkpoint should have moved + await eventStore.AppendToStream("stream-1", ExpectedVersion.Any, CreateNewStreamEvents(7, 8, 9)); + allEventsPage = await eventStore.ReadAll(currentCheckpoint, 10); + allEventsPage.IsEnd.Should().BeTrue(); + allEventsPage.NextCheckpoint.Should().NotBeNull(); + allEventsPage.NextCheckpoint.Should().NotBe(currentCheckpoint.Value); + } + } + } + [Fact] public async Task Can_read_all_backward() { From 2e4a5dc3fed0bd1d1ceb8b47c9562ed2a16c82f2 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Tue, 28 Jul 2015 10:37:01 +0100 Subject: [PATCH 21/34] Fixup test for reading past end of stream - GES may hand back an empty collection of events but will eventually return the end of the stream, with a checkpoint. --- .../EventStoreAcceptanceTests.ReadAll.cs | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadAll.cs b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadAll.cs index 6679bfa01..167db8400 100644 --- a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadAll.cs +++ b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadAll.cs @@ -1,5 +1,6 @@ namespace Cedar.EventStore { + using System; using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; @@ -75,14 +76,24 @@ public async Task Read_forwards_to_the_end_should_return_a_valid_Checkpoint() // read end of stream again, should be empty, should return same checkpoint allEventsPage = await eventStore.ReadAll(currentCheckpoint, 10); + while (!allEventsPage.IsEnd) + { + allEventsPage = await eventStore.ReadAll(allEventsPage.NextCheckpoint, 10); + } + allEventsPage.StreamEvents.Should().BeEmpty(); allEventsPage.IsEnd.Should().BeTrue(); allEventsPage.NextCheckpoint.Should().NotBeNull(); - allEventsPage.NextCheckpoint.Should().Be(currentCheckpoint.Value); // append some events then read again from the saved checkpoint, the next checkpoint should have moved await eventStore.AppendToStream("stream-1", ExpectedVersion.Any, CreateNewStreamEvents(7, 8, 9)); + allEventsPage = await eventStore.ReadAll(currentCheckpoint, 10); + while (!allEventsPage.IsEnd) + { + allEventsPage = await eventStore.ReadAll(allEventsPage.NextCheckpoint, 10); + } + allEventsPage.IsEnd.Should().BeTrue(); allEventsPage.NextCheckpoint.Should().NotBeNull(); allEventsPage.NextCheckpoint.Should().NotBe(currentCheckpoint.Value); @@ -90,6 +101,42 @@ public async Task Read_forwards_to_the_end_should_return_a_valid_Checkpoint() } } + [Fact] + public async Task When_read_past_end_of_all() + { + using(var fixture = GetFixture()) + { + using(var eventStore = await fixture.GetEventStore()) + { + await eventStore.AppendToStream("stream-1", ExpectedVersion.NoStream, CreateNewStreamEvents(1, 2, 3)); + + bool isEnd = false; + int count = 0; + Checkpoint checkpoint = Checkpoint.Start; + while (!isEnd) + { + _testOutputHelper.WriteLine($"Loop {count}"); + + var streamEventsPage = await eventStore.ReadAll(checkpoint, 10); + _testOutputHelper.WriteLine($"FromCheckpoint = {streamEventsPage.FromCheckpoint}"); + _testOutputHelper.WriteLine($"NextCheckpoint = {streamEventsPage.NextCheckpoint}"); + _testOutputHelper.WriteLine($"IsEnd = {streamEventsPage.IsEnd}"); + _testOutputHelper.WriteLine($"StreamEvents.Count = {streamEventsPage.StreamEvents.Count}"); + _testOutputHelper.WriteLine(""); + + checkpoint = streamEventsPage.NextCheckpoint; + isEnd = streamEventsPage.IsEnd; + count++; + + if(count > 100) + { + throw new Exception("omg wtf"); + } + } + } + } + } + [Fact] public async Task Can_read_all_backward() { From 6d41c4e228b58f0755a694ac4c0783197ea5518e Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Tue, 28 Jul 2015 10:45:12 +0100 Subject: [PATCH 22/34] Add counter to short circuit run-away implementations --- .../EventStoreAcceptanceTests.ReadAll.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadAll.cs b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadAll.cs index 167db8400..bfd5a2dfb 100644 --- a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadAll.cs +++ b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.ReadAll.cs @@ -64,9 +64,13 @@ public async Task Read_forwards_to_the_end_should_return_a_valid_Checkpoint() // read to the end of the stream var allEventsPage = await eventStore.ReadAll(Checkpoint.Start, 4); - while (!allEventsPage.IsEnd) + + int count = 0; //counter is used to short circuit bad implementations that never return IsEnd = true + + while (!allEventsPage.IsEnd && count < 20) { allEventsPage = await eventStore.ReadAll(allEventsPage.NextCheckpoint, 10); + count++; } allEventsPage.IsEnd.Should().BeTrue(); @@ -76,22 +80,28 @@ public async Task Read_forwards_to_the_end_should_return_a_valid_Checkpoint() // read end of stream again, should be empty, should return same checkpoint allEventsPage = await eventStore.ReadAll(currentCheckpoint, 10); - while (!allEventsPage.IsEnd) + count = 0; + while (!allEventsPage.IsEnd && count < 20) { allEventsPage = await eventStore.ReadAll(allEventsPage.NextCheckpoint, 10); + count++; } allEventsPage.StreamEvents.Should().BeEmpty(); allEventsPage.IsEnd.Should().BeTrue(); allEventsPage.NextCheckpoint.Should().NotBeNull(); + currentCheckpoint = allEventsPage.NextCheckpoint; + // append some events then read again from the saved checkpoint, the next checkpoint should have moved await eventStore.AppendToStream("stream-1", ExpectedVersion.Any, CreateNewStreamEvents(7, 8, 9)); allEventsPage = await eventStore.ReadAll(currentCheckpoint, 10); - while (!allEventsPage.IsEnd) + count = 0; + while (!allEventsPage.IsEnd && count < 20) { allEventsPage = await eventStore.ReadAll(allEventsPage.NextCheckpoint, 10); + count++; } allEventsPage.IsEnd.Should().BeTrue(); From 99f17d4b353e83a34e869b82825cb01941dbc445 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Tue, 28 Jul 2015 11:30:46 +0100 Subject: [PATCH 23/34] Make data + metadata proper JSON for postgres --- src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.cs b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.cs index 12bbd42ce..00f57a551 100644 --- a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.cs +++ b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.cs @@ -50,7 +50,7 @@ private static NewStreamEvent[] CreateNewStreamEvents(params int[] eventNumbers) .Select(eventNumber => { var eventId = Guid.Parse("00000000-0000-0000-0000-" + eventNumber.ToString().PadLeft(12, '0')); - return new NewStreamEvent(eventId, "type", "data", "metadata"); + return new NewStreamEvent(eventId, "type", "\"data\"", "\"metadata\""); }) .ToArray(); } @@ -62,7 +62,7 @@ private static StreamEvent ExpectedStreamEvent( DateTime created) { var eventId = Guid.Parse("00000000-0000-0000-0000-" + eventNumber.ToString().PadLeft(12, '0')); - return new StreamEvent(streamId, eventId, sequenceNumber, null, created, "type", "data", "metadata"); + return new StreamEvent(streamId, eventId, sequenceNumber, null, created, "type", "\"data\"", "\"metadata\""); } } } \ No newline at end of file From 241f238a0c9d91a00414a50fad440ac2126c16bc Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Mon, 3 Aug 2015 14:15:11 +0100 Subject: [PATCH 24/34] Handle serializable transaction rollbacks as WrongExpectedVersionException --- .../PostgresEventStore.cs | 61 ++++++++++++------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs index db5b7d957..13e36d307 100644 --- a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -71,14 +71,14 @@ public async Task AppendToStream( { try { - using( + using ( var command = new NpgsqlCommand(Scripts.Functions.CreateStream, connection, tx) - { - CommandType + { + CommandType = CommandType .StoredProcedure - }) + }) { command.Parameters.AddWithValue(":stream_id", streamIdInfo.StreamId); command.Parameters.AddWithValue(":stream_id_original", streamIdInfo.StreamIdOriginal); @@ -93,8 +93,9 @@ public async Task AppendToStream( if(ex.Code == "23505") { + //not found throw new WrongExpectedVersionException( - Messages.AppendFailedWrongExpectedVersion.FormatWith(streamId, expectedVersion)); + Messages.AppendFailedWrongExpectedVersion.FormatWith(streamId, expectedVersion), ex); } throw; @@ -133,29 +134,45 @@ public async Task AppendToStream( } } - using( - var writer = - connection.BeginBinaryImport(Scripts.BulkCopyEvents) - ) + try { - foreach(var @event in events) + using ( + var writer = + connection.BeginBinaryImport(Scripts.BulkCopyEvents) + ) { - if(cancellationToken.IsCancellationRequested) + foreach (var @event in events) { - writer.Cancel(); - tx.Rollback(); + if (cancellationToken.IsCancellationRequested) + { + writer.Cancel(); + tx.Rollback(); + } + + currentVersion++; + writer.StartRow(); + writer.Write(streamIdInternal, NpgsqlDbType.Integer); + writer.Write(currentVersion, NpgsqlDbType.Integer); + writer.Write(@event.EventId, NpgsqlDbType.Uuid); + writer.Write(SystemClock.GetUtcNow(), NpgsqlDbType.TimestampTZ); + writer.Write(@event.Type); + writer.Write(@event.JsonData, NpgsqlDbType.Json); + writer.Write(@event.JsonMetadata, NpgsqlDbType.Json); } + } + } + catch (NpgsqlException ex) + { + tx.Rollback(); - currentVersion++; - writer.StartRow(); - writer.Write(streamIdInternal, NpgsqlDbType.Integer); - writer.Write(currentVersion, NpgsqlDbType.Integer); - writer.Write(@event.EventId, NpgsqlDbType.Uuid); - writer.Write(SystemClock.GetUtcNow(), NpgsqlDbType.TimestampTZ); - writer.Write(@event.Type); - writer.Write(@event.JsonData, NpgsqlDbType.Json); - writer.Write(@event.JsonMetadata, NpgsqlDbType.Json); + if (ex.Code == "40001") + { + // could not serialize access due to read/write dependencies among transactions + throw new WrongExpectedVersionException( + Messages.AppendFailedWrongExpectedVersion.FormatWith(streamId, expectedVersion), ex); } + + throw; } tx.Commit(); From 3c77b390af35547e6aa36d07f5e49cd15092b792 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Fri, 4 Sep 2015 09:43:22 +0100 Subject: [PATCH 25/34] Create stream where not exists with ExpectedVersion.Any --- .../PostgresEventStore.cs | 21 +++++++++++++++++++ .../EventStoreAcceptanceTests.AppendStream.cs | 16 ++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs index 13e36d307..aed0a68b9 100644 --- a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -132,6 +132,27 @@ public async Task AppendToStream( throw new WrongExpectedVersionException( Messages.AppendFailedWrongExpectedVersion.FormatWith(streamId, expectedVersion)); } + + if(streamIdInternal == -1) + { + // create the stream as it doesn't exist + + using ( + var command = new NpgsqlCommand(Scripts.Functions.CreateStream, connection, tx) + { + CommandType + = + CommandType + .StoredProcedure + }) + { + command.Parameters.AddWithValue(":stream_id", streamIdInfo.StreamId); + command.Parameters.AddWithValue(":stream_id_original", streamIdInfo.StreamIdOriginal); + + streamIdInternal = + (int)await command.ExecuteScalarAsync(cancellationToken).NotOnCapturedContext(); + } + } } try diff --git a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.AppendStream.cs b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.AppendStream.cs index 0f5368a7c..4a3d73350 100644 --- a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.AppendStream.cs +++ b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.AppendStream.cs @@ -97,5 +97,21 @@ await eventStore } } } + + [Fact] + public async Task When_append_stream_with_expected_version_any_and_no_stream_exists_should_not_throw() + { + // Idempotency + using (var fixture = GetFixture()) + { + using (var eventStore = await fixture.GetEventStore()) + { + const string streamId = "stream-1"; + await eventStore + .AppendToStream(streamId, ExpectedVersion.Any, CreateNewStreamEvents(1, 2, 3)) + .ShouldNotThrow(); + } + } + } } } From 35bc4dbb12b824890718b117310a456692acb9a5 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Tue, 8 Sep 2015 17:05:20 +0100 Subject: [PATCH 26/34] Move tx.Commit() inside try block as exceptions are thrown at this point --- src/Cedar.EventStore.Postgres/PostgresEventStore.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs index aed0a68b9..7459fae9f 100644 --- a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -25,7 +25,9 @@ public class PostgresEventStore : IEventStore private readonly InterlockedBoolean _isDisposed = new InterlockedBoolean(); - public PostgresEventStore(string connectionStringOrConnectionStringName) + private readonly int _concurrencyFailureRetryAttempts; + + public PostgresEventStore(string connectionStringOrConnectionStringName, int concurrencyFailureRetryAttempts = 20) { if(connectionStringOrConnectionStringName.IndexOf(';') > -1) { @@ -46,6 +48,8 @@ public PostgresEventStore(string connectionStringOrConnectionStringName) return connection; }; } + + _concurrencyFailureRetryAttempts = concurrencyFailureRetryAttempts; } public async Task AppendToStream( @@ -154,7 +158,7 @@ public async Task AppendToStream( } } } - + try { using ( @@ -181,6 +185,8 @@ public async Task AppendToStream( writer.Write(@event.JsonMetadata, NpgsqlDbType.Json); } } + + tx.Commit(); } catch (NpgsqlException ex) { @@ -195,8 +201,6 @@ public async Task AppendToStream( throw; } - - tx.Commit(); } } From 9f271e6cbb1400d5da9bb932abd52f74b89d0ab6 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Mon, 14 Sep 2015 11:35:04 +0100 Subject: [PATCH 27/34] Add support for buckets/schemas --- .../Cedar.EventStore.Postgres.Tests.csproj | 1 + ...ventStore.Postgres.Tests.v2.ncrunchproject | Bin 2838 -> 2804 bytes .../PostgresEventStoreFixture.cs | 15 +- .../PostgresEventStoreTests.cs | 2 + .../SecondarySchemaTests.cs | 30 +++ ...edar.EventStore.Postgres.v2.ncrunchproject | Bin 2964 -> 2804 bytes .../PostgresEventStore.cs | 28 +-- .../SqlScripts/BulkCopyEvents.sql | 2 +- .../SqlScripts/CreateStream.sql | 2 +- .../SqlScripts/DropAll.sql | 6 +- .../SqlScripts/GetStream.sql | 4 +- .../SqlScripts/InitializeStore.sql | 208 +++++++++--------- .../SqlScripts/ReadAllBackward.sql | 4 +- .../SqlScripts/ReadAllForward.sql | 4 +- .../SqlScripts/Scripts.cs | 79 ++++--- .../EventStoreAcceptanceTests.cs | 4 +- 16 files changed, 227 insertions(+), 162 deletions(-) create mode 100644 src/Cedar.EventStore.Postgres.Tests/SecondarySchemaTests.cs diff --git a/src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.csproj b/src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.csproj index 5c6f32387..af3c893a4 100644 --- a/src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.csproj +++ b/src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.csproj @@ -62,6 +62,7 @@ + diff --git a/src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.v2.ncrunchproject b/src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.v2.ncrunchproject index 2ee0c8cbbe20200e8ac79b1ac3b630a98bdc8ed6..724068ebc7d28c10e685b4ba89a7a50161aeaa68 100644 GIT binary patch delta 16 XcmbOx_C<693n#MzgZ^es&bN#JDmMg} delta 22 ecmew&I!$Z?3n!}`gAIfJje&FaiKfeg<&> diff --git a/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreFixture.cs b/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreFixture.cs index aec05c1ee..a7ec33fa1 100644 --- a/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreFixture.cs +++ b/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreFixture.cs @@ -4,9 +4,20 @@ namespace Cedar.EventStore.Postgres.Tests public class PostgresEventStoreFixture : EventStoreAcceptanceTestFixture { - public override async Task GetEventStore() + private readonly string _schema; + public PostgresEventStoreFixture(string schema = "public") { - var eventStore = new PostgresEventStore(@"Server=127.0.0.1;Port=5432;Database=cedar_tests;User Id=postgres;Password=postgres;"); + _schema = schema; + } + + public override Task GetEventStore() + { + return GetEventStore(_schema); + } + + private async Task GetEventStore(string schema) + { + var eventStore = new PostgresEventStore(@"Server=127.0.0.1;Port=5432;Database=cedar_tests;User Id=postgres;Password=postgres;", schema); await eventStore.DropAll(ignoreErrors: true); await eventStore.InitializeStore(); diff --git a/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreTests.cs b/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreTests.cs index 75bd38634..8563f64a5 100644 --- a/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreTests.cs +++ b/src/Cedar.EventStore.Postgres.Tests/PostgresEventStoreTests.cs @@ -1,5 +1,7 @@ namespace Cedar.EventStore.Postgres.Tests { + using System.Threading.Tasks; + using Xunit; using Xunit.Abstractions; public class PostgresEventStoreTests : EventStoreAcceptanceTests diff --git a/src/Cedar.EventStore.Postgres.Tests/SecondarySchemaTests.cs b/src/Cedar.EventStore.Postgres.Tests/SecondarySchemaTests.cs new file mode 100644 index 000000000..ac6565e3d --- /dev/null +++ b/src/Cedar.EventStore.Postgres.Tests/SecondarySchemaTests.cs @@ -0,0 +1,30 @@ +namespace Cedar.EventStore.Postgres.Tests +{ + using System.Threading.Tasks; + using Xunit; + using Xunit.Abstractions; + + public class SecondarySchemaTests : EventStoreAcceptanceTests + { + public SecondarySchemaTests(ITestOutputHelper testOutputHelper) + : base(testOutputHelper) + {} + + protected override EventStoreAcceptanceTestFixture GetFixture() + { + return new PostgresEventStoreFixture("secondary_schema"); + } + + [Fact] + public async Task can_store_events_in_different_schemas() + { + using (var defaultStore = await GetFixture().GetEventStore()) + using (var secondaryStore = await new PostgresEventStoreFixture("saga_events").GetEventStore()) + { + const string streamId = "stream-1"; + await defaultStore.AppendToStream(streamId, ExpectedVersion.NoStream, CreateNewStreamEvents(1, 2, 3)); + await secondaryStore.AppendToStream(streamId, ExpectedVersion.NoStream, CreateNewStreamEvents(1, 2, 3)); + } + } + } +} \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.v2.ncrunchproject b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.v2.ncrunchproject index f14526cf83386d3d740ab04ba7a6314f48bd1326..724068ebc7d28c10e685b4ba89a7a50161aeaa68 100644 GIT binary patch delta 20 bcmbOt{zY^H3n#MzgZ^es&i#y&)425jKQING delta 136 zcmew&Iz@Z~3n!}`gAIfJjf5F-|_m$u{{7Gf!#?LkL4ELoq`M5Zf{EGH@{{ yK%otTBSQ`lBIJXCDszB18K^p+p$Mo>A4w0;cm_{~bcQ^zTq*;S$&>eUnF0Xl{~J{R diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs index 7459fae9f..2dc253bb7 100644 --- a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -25,9 +25,9 @@ public class PostgresEventStore : IEventStore private readonly InterlockedBoolean _isDisposed = new InterlockedBoolean(); - private readonly int _concurrencyFailureRetryAttempts; + private readonly Scripts _scripts; - public PostgresEventStore(string connectionStringOrConnectionStringName, int concurrencyFailureRetryAttempts = 20) + public PostgresEventStore(string connectionStringOrConnectionStringName, string schema = "public") { if(connectionStringOrConnectionStringName.IndexOf(';') > -1) { @@ -49,7 +49,7 @@ public PostgresEventStore(string connectionStringOrConnectionStringName, int con }; } - _concurrencyFailureRetryAttempts = concurrencyFailureRetryAttempts; + _scripts = new Scripts(schema); } public async Task AppendToStream( @@ -76,7 +76,7 @@ public async Task AppendToStream( try { using ( - var command = new NpgsqlCommand(Scripts.Functions.CreateStream, connection, tx) + var command = new NpgsqlCommand(_scripts.Functions.CreateStream, connection, tx) { CommandType = @@ -107,7 +107,7 @@ public async Task AppendToStream( } else { - using (var command = new NpgsqlCommand(Scripts.Functions.GetStream, connection, tx) { CommandType = CommandType.StoredProcedure }) + using (var command = new NpgsqlCommand(_scripts.Functions.GetStream, connection, tx) { CommandType = CommandType.StoredProcedure }) { command.Parameters.AddWithValue(":stream_id", streamIdInfo.StreamId); @@ -142,7 +142,7 @@ public async Task AppendToStream( // create the stream as it doesn't exist using ( - var command = new NpgsqlCommand(Scripts.Functions.CreateStream, connection, tx) + var command = new NpgsqlCommand(_scripts.Functions.CreateStream, connection, tx) { CommandType = @@ -163,7 +163,7 @@ public async Task AppendToStream( { using ( var writer = - connection.BeginBinaryImport(Scripts.BulkCopyEvents) + connection.BeginBinaryImport(_scripts.BulkCopyEvents) ) { foreach (var @event in events) @@ -224,7 +224,7 @@ private async Task DeleteStreamAnyVersion( CancellationToken cancellationToken) { using (var connection = await _createAndOpenConnection()) - using (var command = new NpgsqlCommand(Scripts.Functions.DeleteStreamAnyVersion, connection) { CommandType = CommandType.StoredProcedure }) + using (var command = new NpgsqlCommand(_scripts.Functions.DeleteStreamAnyVersion, connection) { CommandType = CommandType.StoredProcedure }) { command.Parameters.AddWithValue("stream_id", streamIdInfo.StreamId); await command @@ -240,7 +240,7 @@ private async Task DeleteStreamExpectedVersion( CancellationToken cancellationToken) { using (var connection = await _createAndOpenConnection()) - using (var command = new NpgsqlCommand(Scripts.Functions.DeleteStreamExpectedVersion, connection) { CommandType = CommandType.StoredProcedure }) + using (var command = new NpgsqlCommand(_scripts.Functions.DeleteStreamExpectedVersion, connection) { CommandType = CommandType.StoredProcedure }) { command.Parameters.AddWithValue("stream_id", streamIdInfo.StreamId); command.Parameters.AddWithValue("expected_version", expectedVersion); @@ -277,7 +277,7 @@ public async Task ReadAll( long ordinal = checkpoint.GetOrdinal(); - var commandText = direction == ReadDirection.Forward ? Scripts.ReadAllForward : Scripts.ReadAllBackward; + var commandText = direction == ReadDirection.Forward ? _scripts.ReadAllForward : _scripts.ReadAllBackward; using (var connection = await _createAndOpenConnection()) using (var command = new NpgsqlCommand(commandText, connection))// { CommandType = CommandType.StoredProcedure }) @@ -367,12 +367,12 @@ public async Task ReadStream( Func, int> getNextSequenceNumber; if(direction == ReadDirection.Forward) { - commandText = Scripts.Functions.ReadStreamForward; + commandText = _scripts.Functions.ReadStreamForward; getNextSequenceNumber = events => events.Count > 0 ? events.Last().StreamVersion + 1 : -1; //todo: review this } else { - commandText = Scripts.Functions.ReadStreamBackward; + commandText = _scripts.Functions.ReadStreamBackward; getNextSequenceNumber = events => events.Count > 0 ? events.Last().StreamVersion - 1 : -1; //todo: review this } @@ -464,7 +464,7 @@ public async Task InitializeStore( CancellationToken cancellationToken = default(CancellationToken)) { using (var connection = await _createAndOpenConnection()) - using(var cmd = new NpgsqlCommand(Scripts.InitializeStore, connection)) + using(var cmd = new NpgsqlCommand(_scripts.InitializeStore, connection)) { if (ignoreErrors) { @@ -484,7 +484,7 @@ public async Task DropAll( CancellationToken cancellationToken = default(CancellationToken)) { using (var connection = await _createAndOpenConnection()) - using(var cmd = new NpgsqlCommand(Scripts.DropAll, connection)) + using(var cmd = new NpgsqlCommand(_scripts.DropAll, connection)) { if (ignoreErrors) { diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/BulkCopyEvents.sql b/src/Cedar.EventStore.Postgres/SqlScripts/BulkCopyEvents.sql index a209a307e..f422b0592 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/BulkCopyEvents.sql +++ b/src/Cedar.EventStore.Postgres/SqlScripts/BulkCopyEvents.sql @@ -1,2 +1,2 @@ -COPY events (stream_id_internal, stream_version, id, created, type, json_data, json_metadata) +COPY $schema$.events (stream_id_internal, stream_version, id, created, type, json_data, json_metadata) FROM STDIN BINARY \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/CreateStream.sql b/src/Cedar.EventStore.Postgres/SqlScripts/CreateStream.sql index 3b706ad01..1eacf7cde 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/CreateStream.sql +++ b/src/Cedar.EventStore.Postgres/SqlScripts/CreateStream.sql @@ -1,3 +1,3 @@ -INSERT INTO streams(id, id_original) +INSERT INTO $schema$.streams(id, id_original) VALUES (:stream_id, :stream_id_original) RETURNING id_internal; \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/DropAll.sql b/src/Cedar.EventStore.Postgres/SqlScripts/DropAll.sql index 1c7a1c164..dd097b90a 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/DropAll.sql +++ b/src/Cedar.EventStore.Postgres/SqlScripts/DropAll.sql @@ -1,3 +1,3 @@ -DROP TABLE IF EXISTS events; -DROP TABLE IF EXISTS streams; -DROP TYPE IF EXISTS new_stream_events; \ No newline at end of file +DROP TABLE IF EXISTS $schema$.events; +DROP TABLE IF EXISTS $schema$.streams; +DROP TYPE IF EXISTS $schema$.new_stream_events; \ No newline at end of file diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/GetStream.sql b/src/Cedar.EventStore.Postgres/SqlScripts/GetStream.sql index c91287746..0cd6f7ffc 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/GetStream.sql +++ b/src/Cedar.EventStore.Postgres/SqlScripts/GetStream.sql @@ -1,8 +1,8 @@ SELECT streams.id_internal, streams.is_deleted, events.stream_version -FROM streams -LEFT JOIN events +FROM $schema$.streams +LEFT JOIN $schema$.events ON events.stream_id_internal = streams.id_internal WHERE streams.id = :stream_id ORDER BY events.ordinal DESC diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql index 9046c8c38..266d9e321 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql +++ b/src/Cedar.EventStore.Postgres/SqlScripts/InitializeStore.sql @@ -1,4 +1,8 @@ -CREATE TABLE streams( + + +CREATE SCHEMA IF NOT EXISTS $schema$; + +CREATE TABLE $schema$.streams( id_internal SERIAL PRIMARY KEY, id text NOT NULL, id_original text NOT NULL, @@ -6,10 +10,10 @@ CREATE TABLE streams( ); CREATE UNIQUE INDEX ix_streams_id -ON streams +ON $schema$.streams USING btree(id); -CREATE TABLE events( +CREATE TABLE $schema$.events( stream_id_internal integer NOT NULL, stream_version integer NOT NULL, ordinal SERIAL PRIMARY KEY NOT NULL , @@ -18,20 +22,20 @@ CREATE TABLE events( type text NOT NULL, json_data json NOT NULL, json_metadata json , - CONSTRAINT fk_events_streams FOREIGN KEY (stream_id_internal) REFERENCES streams(id_internal) + CONSTRAINT fk_events_streams FOREIGN KEY (stream_id_internal) REFERENCES $schema$.streams(id_internal) ); CREATE UNIQUE INDEX ix_events_stream_id_internal_revision -ON events +ON $schema$.events USING btree(stream_id_internal, stream_version DESC, ordinal DESC); -CREATE OR REPLACE FUNCTION create_stream(_stream_id text, _stream_id_original text) +CREATE OR REPLACE FUNCTION $schema$.create_stream(_stream_id text, _stream_id_original text) RETURNS integer AS $BODY$ DECLARE _result integer; BEGIN - INSERT INTO streams(id, id_original) + INSERT INTO $schema$.streams(id, id_original) VALUES (_stream_id, _stream_id_original) RETURNING id_internal INTO _result; @@ -41,70 +45,70 @@ END; $BODY$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION get_stream(_stream_id text) +CREATE OR REPLACE FUNCTION $schema$.get_stream(_stream_id text) RETURNS TABLE(id_internal integer, is_deleted boolean, stream_version integer) AS $BODY$ BEGIN RETURN QUERY - SELECT streams.id_internal, - streams.is_deleted, - (SELECT max(events.stream_version) from events where events.stream_id_internal = streams.id_internal) - FROM streams - WHERE streams.id = _stream_id; + SELECT $schema$.streams.id_internal, + $schema$.streams.is_deleted, + (SELECT max($schema$.events.stream_version) from $schema$.events where $schema$.events.stream_id_internal = $schema$.streams.id_internal) + FROM $schema$.streams + WHERE $schema$.streams.id = _stream_id; END; $BODY$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION read_all_forward(_ordinal bigint, _count integer) +CREATE OR REPLACE FUNCTION $schema$.read_all_forward(_ordinal bigint, _count integer) RETURNS TABLE(stream_id integer, stream_version integer, ordinal bigint, event_id uuid, created timestamp, type text, json_data json, json_metadata json) AS $BODY$ BEGIN RETURN QUERY SELECT - streams.id_original As stream_id, - events.stream_version, - events.ordinal, - events.id AS event_id, - events.created, - events.type, - events.json_data, - events.json_metadata - FROM events - INNER JOIN streams - ON events.stream_id_internal = streams.id_internal - WHERE events.ordinal >= _ordinal - ORDER BY events.ordinal + $schema$.streams.id_original As stream_id, + $schema$.events.stream_version, + $schema$.events.ordinal, + $schema$.events.id AS event_id, + $schema$.events.created, + $schema$.events.type, + $schema$.events.json_data, + $schema$.events.json_metadata + FROM $schema$.events + INNER JOIN $schema$.streams + ON $schema$.events.stream_id_internal = $schema$.streams.id_internal + WHERE $schema$.events.ordinal >= _ordinal + ORDER BY $schema$.events.ordinal LIMIT _count; END; $BODY$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION read_all_backward(_ordinal bigint, _count integer) +CREATE OR REPLACE FUNCTION $schema$.read_all_backward(_ordinal bigint, _count integer) RETURNS TABLE(stream_id integer, stream_version integer, ordinal bigint, event_id uuid, created timestamp, type text, json_data json, json_metadata json) AS $BODY$ BEGIN RETURN QUERY SELECT - streams.id_original As stream_id, - events.stream_version, - events.ordinal, - events.id AS event_id, - events.created, - events.type, - events.json_data, - events.json_metadata - FROM events - INNER JOIN streams - ON events.stream_id_internal = streams.id_internal - WHERE events.ordinal <= _ordinal - ORDER BY events.ordinal DESC + $schema$.streams.id_original As stream_id, + $schema$.events.stream_version, + $schema$.events.ordinal, + $schema$.events.id AS event_id, + $schema$.events.created, + $schema$.events.type, + $schema$.events.json_data, + $schema$.events.json_metadata + FROM $schema$.events + INNER JOIN $schema$.streams + ON $schema$.events.stream_id_internal = $schema$.streams.id_internal + WHERE $schema$.events.ordinal <= _ordinal + ORDER BY $schema$.events.ordinal DESC LIMIT _count; END; $BODY$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION read_stream_forward(_stream_id text, _count integer, _stream_version integer) RETURNS SETOF refcursor AS +CREATE OR REPLACE FUNCTION $schema$.read_stream_forward(_stream_id text, _count integer, _stream_version integer) RETURNS SETOF refcursor AS $BODY$ DECLARE ref1 refcursor; @@ -113,10 +117,10 @@ DECLARE _stream_id_internal integer; _is_deleted boolean; BEGIN -SELECT streams.id_internal, streams.is_deleted +SELECT $schema$.streams.id_internal, $schema$.streams.is_deleted INTO _stream_id_internal, _is_deleted - FROM streams - WHERE streams.id = _stream_id; + FROM $schema$.streams + WHERE $schema$.streams.id = _stream_id; OPEN ref1 FOR SELECT _stream_id_internal, _is_deleted; @@ -125,27 +129,27 @@ RETURN NEXT ref1; OPEN ref2 FOR SELECT - events.stream_version, - events.ordinal, - events.id AS event_id, - events.created, - events.type, - events.json_data, - events.json_metadata - FROM events - INNER JOIN streams - ON events.stream_id_internal = streams.id_internal - WHERE events.stream_id_internal = _stream_id_internal - AND events.stream_version >= _stream_version - ORDER BY events.ordinal + $schema$.events.stream_version, + $schema$.events.ordinal, + $schema$.events.id AS event_id, + $schema$.events.created, + $schema$.events.type, + $schema$.events.json_data, + $schema$.events.json_metadata + FROM $schema$.events + INNER JOIN $schema$.streams + ON $schema$.events.stream_id_internal = $schema$.streams.id_internal + WHERE $schema$.events.stream_id_internal = _stream_id_internal + AND $schema$.events.stream_version >= _stream_version + ORDER BY $schema$.events.ordinal LIMIT _count; RETURN next ref2; OPEN ref3 FOR - SELECT events.stream_version - FROM events - WHERE events.stream_id_internal = _stream_id_internal - ORDER BY events.ordinal DESC + SELECT $schema$.events.stream_version + FROM $schema$.events + WHERE $schema$.events.stream_id_internal = _stream_id_internal + ORDER BY $schema$.events.ordinal DESC LIMIT 1; RETURN next ref3; @@ -154,7 +158,7 @@ END; $BODY$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION read_stream_backward(_stream_id text, _count integer, _stream_version integer) RETURNS SETOF refcursor AS +CREATE OR REPLACE FUNCTION $schema$.read_stream_backward(_stream_id text, _count integer, _stream_version integer) RETURNS SETOF refcursor AS $BODY$ DECLARE ref1 refcursor; @@ -164,10 +168,10 @@ DECLARE _is_deleted boolean; BEGIN -SELECT streams.id_internal, streams.is_deleted +SELECT $schema$.streams.id_internal, $schema$.streams.is_deleted INTO _stream_id_internal, _is_deleted - FROM streams - WHERE streams.id = _stream_id; + FROM $schema$.streams + WHERE $schema$.streams.id = _stream_id; OPEN ref1 FOR SELECT _stream_id_internal, _is_deleted; @@ -176,27 +180,27 @@ RETURN NEXT ref1; OPEN ref2 FOR SELECT - events.stream_version, - events.ordinal, - events.id AS event_id, - events.created, - events.type, - events.json_data, - events.json_metadata - FROM events - INNER JOIN streams - ON events.stream_id_internal = streams.id_internal - WHERE events.stream_id_internal = _stream_id_internal - AND events.stream_version <= _stream_version - ORDER BY events.ordinal DESC + $schema$.events.stream_version, + $schema$.events.ordinal, + $schema$.events.id AS event_id, + $schema$.events.created, + $schema$.events.type, + $schema$.events.json_data, + $schema$.events.json_metadata + FROM $schema$.events + INNER JOIN $schema$.streams + ON $schema$.events.stream_id_internal = $schema$.streams.id_internal + WHERE $schema$.events.stream_id_internal = _stream_id_internal + AND $schema$.events.stream_version <= _stream_version + ORDER BY $schema$.events.ordinal DESC LIMIT _count; RETURN next ref2; OPEN ref3 FOR - SELECT events.stream_version - FROM events - WHERE events.stream_id_internal = _stream_id_internal - ORDER BY events.ordinal DESC + SELECT $schema$.events.stream_version + FROM $schema$.events + WHERE $schema$.events.stream_id_internal = _stream_id_internal + ORDER BY $schema$.events.ordinal DESC LIMIT 1; RETURN next ref3; @@ -206,23 +210,23 @@ $BODY$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION delete_stream_any_version(stream_id text) RETURNS VOID AS +CREATE OR REPLACE FUNCTION $schema$.delete_stream_any_version(stream_id text) RETURNS VOID AS $BODY$ DECLARE _stream_id_internal integer; BEGIN - SELECT streams.id_internal + SELECT $schema$.streams.id_internal INTO _stream_id_internal - FROM streams - WHERE streams.id = stream_id; + FROM $schema$.streams + WHERE $schema$.streams.id = stream_id; - DELETE FROM events - WHERE events.stream_id_internal = _stream_id_internal; + DELETE FROM $schema$.events + WHERE $schema$.events.stream_id_internal = _stream_id_internal; - UPDATE streams + UPDATE $schema$.streams SET is_deleted = true - WHERE streams.id_internal = _stream_id_internal; + WHERE $schema$.streams.id_internal = _stream_id_internal; RETURN; END; @@ -230,17 +234,17 @@ $BODY$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION delete_stream_expected_version(stream_id text, expected_version integer) RETURNS VOID AS +CREATE OR REPLACE FUNCTION $schema$.delete_stream_expected_version(stream_id text, expected_version integer) RETURNS VOID AS $BODY$ DECLARE _stream_id_internal integer; _lastest_stream_version integer; BEGIN - SELECT streams.id_internal + SELECT $schema$.streams.id_internal INTO _stream_id_internal - FROM streams - WHERE streams.id = stream_id; + FROM $schema$.streams + WHERE $schema$.streams.id = stream_id; IF _stream_id_internal IS NULL THEN RAISE EXCEPTION 'WrongExpectedVersion' @@ -249,9 +253,9 @@ BEGIN SELECT stream_version INTO _lastest_stream_version - FROM events - WHERE events.stream_id_internal = _stream_id_internal - ORDER BY events.ordinal DESC + FROM $schema$.events + WHERE $schema$.events.stream_id_internal = _stream_id_internal + ORDER BY $schema$.events.ordinal DESC LIMIT 1; IF (_lastest_stream_version <> expected_version) THEN @@ -259,12 +263,12 @@ BEGIN USING HINT = 'The Stream ' || stream_id || 'version was expected to be' || expected_version::text || ' but was version ' || _lastest_stream_version::text || '.' ; END IF; - DELETE FROM events - WHERE events.stream_id_internal = _stream_id_internal; + DELETE FROM $schema$.events + WHERE $schema$.events.stream_id_internal = _stream_id_internal; - UPDATE streams + UPDATE $schema$.streams SET is_deleted = true - WHERE streams.id_internal = _stream_id_internal; + WHERE $schema$.streams.id_internal = _stream_id_internal; RETURN; END; diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/ReadAllBackward.sql b/src/Cedar.EventStore.Postgres/SqlScripts/ReadAllBackward.sql index 8430a8be2..1fc9c7507 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/ReadAllBackward.sql +++ b/src/Cedar.EventStore.Postgres/SqlScripts/ReadAllBackward.sql @@ -7,8 +7,8 @@ SELECT events.type, events.json_data, events.json_metadata - FROM events - INNER JOIN streams + FROM $schema$.events + INNER JOIN $schema$.streams ON events.stream_id_internal = streams.id_internal WHERE events.ordinal <= :ordinal ORDER BY events.ordinal DESC diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/ReadAllForward.sql b/src/Cedar.EventStore.Postgres/SqlScripts/ReadAllForward.sql index 08b6ec1ee..1a03964a6 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/ReadAllForward.sql +++ b/src/Cedar.EventStore.Postgres/SqlScripts/ReadAllForward.sql @@ -7,8 +7,8 @@ SELECT events.type, events.json_data, events.json_metadata - FROM events - INNER JOIN streams + FROM $schema$.events + INNER JOIN $schema$.streams ON events.stream_id_internal = streams.id_internal WHERE events.ordinal >= :ordinal ORDER BY events.ordinal diff --git a/src/Cedar.EventStore.Postgres/SqlScripts/Scripts.cs b/src/Cedar.EventStore.Postgres/SqlScripts/Scripts.cs index f6cc3d715..8a77f6734 100644 --- a/src/Cedar.EventStore.Postgres/SqlScripts/Scripts.cs +++ b/src/Cedar.EventStore.Postgres/SqlScripts/Scripts.cs @@ -4,37 +4,47 @@ using System.Collections.Concurrent; using System.IO; - public static class Scripts + public class Scripts { - private static readonly ConcurrentDictionary s_scripts - = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary s_scripts + = new ConcurrentDictionary(); - public static string InitializeStore + private readonly string _schema; + + public Scripts(string schema = "public") { - get { return GetScript("InitializeStore"); } + _schema = schema; + Functions = new GetFunctions(_schema); } - public static string DropAll + public string InitializeStore { - get { return GetScript("DropAll"); } + get { return GetScript("InitializeStore").Replace("$schema$", _schema); } } - public static string BulkCopyEvents + public string DropAll { - get { return GetScript("BulkCopyEvents"); } + get { return GetScript("DropAll").Replace("$schema$", _schema); } } - public static string ReadAllForward + public string BulkCopyEvents { - get { return GetScript("ReadAllForward"); } + get { return GetScript("BulkCopyEvents").Replace("$schema$", _schema); } } - public static string ReadAllBackward + public string ReadAllForward { - get { return GetScript("ReadAllBackward"); } + get { return GetScript("ReadAllForward").Replace("$schema$", _schema); } } - private static string GetScript(string name) + public string ReadAllBackward + { + get { return GetScript("ReadAllBackward").Replace("$schema$", _schema); } + } + + public GetFunctions Functions { get; private set; } + + private string GetScript(string name) { return s_scripts.GetOrAdd(name, key => @@ -55,46 +65,53 @@ private static string GetScript(string name) }); } - public static class Functions + public class GetFunctions { - public static string CreateStream + private readonly string _schema; + + public GetFunctions(string schema) + { + _schema = schema; + } + + public string CreateStream { - get { return "create_stream"; } + get { return string.Concat(_schema, ".", "create_stream"); } } - public static string GetStream + public string GetStream { - get { return "get_stream"; } + get { return string.Concat(_schema, ".", "get_stream"); } } - public static string ReadAllForward + public string ReadAllForward { - get { return "read_all_forward"; } + get { return string.Concat(_schema, ".", "read_all_forward"); } } - public static string ReadAllBackward + public string ReadAllBackward { - get { return "read_all_backward"; } + get { return string.Concat(_schema, ".", "read_all_backward"); } } - public static string ReadStreamForward + public string ReadStreamForward { - get { return "read_stream_forward"; } + get { return string.Concat(_schema, ".", "read_stream_forward"); } } - public static string ReadStreamBackward + public string ReadStreamBackward { - get { return "read_stream_backward"; } + get { return string.Concat(_schema, ".", "read_stream_backward"); } } - public static string DeleteStreamAnyVersion + public string DeleteStreamAnyVersion { - get { return "delete_stream_any_version"; } + get { return string.Concat(_schema, ".", "delete_stream_any_version"); } } - public static string DeleteStreamExpectedVersion + public string DeleteStreamExpectedVersion { - get { return "delete_stream_expected_version"; } + get { return string.Concat(_schema, ".", "delete_stream_expected_version"); } } } } diff --git a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.cs b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.cs index 00f57a551..6dc6b1404 100644 --- a/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.cs +++ b/src/Cedar.EventStore.Tests/EventStoreAcceptanceTests.cs @@ -44,7 +44,7 @@ public async Task Can_dispose_more_than_once() act.ShouldNotThrow(); } } - private static NewStreamEvent[] CreateNewStreamEvents(params int[] eventNumbers) + public static NewStreamEvent[] CreateNewStreamEvents(params int[] eventNumbers) { return eventNumbers .Select(eventNumber => @@ -55,7 +55,7 @@ private static NewStreamEvent[] CreateNewStreamEvents(params int[] eventNumbers) .ToArray(); } - private static StreamEvent ExpectedStreamEvent( + public static StreamEvent ExpectedStreamEvent( string streamId, int eventNumber, int sequenceNumber, From 1f8178964a1e246c3332bbfff8c1b53344375b15 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Thu, 8 Oct 2015 14:23:18 +0100 Subject: [PATCH 28/34] Fixup tx serialization error handling - tx cannot be rolled back as it has already been completed --- .../PostgresEventStore.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs index 2dc253bb7..ebfbbe2a0 100644 --- a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -161,10 +161,7 @@ public async Task AppendToStream( try { - using ( - var writer = - connection.BeginBinaryImport(_scripts.BulkCopyEvents) - ) + using (var writer = connection.BeginBinaryImport(_scripts.BulkCopyEvents) ) { foreach (var @event in events) { @@ -172,6 +169,7 @@ public async Task AppendToStream( { writer.Cancel(); tx.Rollback(); + break; } currentVersion++; @@ -184,14 +182,13 @@ public async Task AppendToStream( writer.Write(@event.JsonData, NpgsqlDbType.Json); writer.Write(@event.JsonMetadata, NpgsqlDbType.Json); } - } - tx.Commit(); + writer.Close(); + tx.Commit(); + } } catch (NpgsqlException ex) { - tx.Rollback(); - if (ex.Code == "40001") { // could not serialize access due to read/write dependencies among transactions @@ -199,6 +196,9 @@ public async Task AppendToStream( Messages.AppendFailedWrongExpectedVersion.FormatWith(streamId, expectedVersion), ex); } + //if error code is 40001 the transaction is already rolled back + tx.Rollback(); + throw; } } From 374162feedc8b05f03b8fb5db358a25991d8e7c9 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Thu, 8 Oct 2015 15:12:18 +0100 Subject: [PATCH 29/34] Add concurrent load test --- ...tore.GetEventStore.Tests.v2.ncrunchproject | Bin 3030 -> 2804 bytes .../ConcurrentLoadTests.cs | 38 ++++++++++++++++++ .../TaskExtensions.cs | 26 ++++++++++++ src/nuget.config | 7 +--- 4 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 src/Cedar.EventStore.Postgres.Tests/ConcurrentLoadTests.cs create mode 100644 src/Cedar.EventStore.Postgres.Tests/TaskExtensions.cs diff --git a/src/Cedar.EventStore.GetEventStore.Tests/Cedar.EventStore.GetEventStore.Tests.v2.ncrunchproject b/src/Cedar.EventStore.GetEventStore.Tests/Cedar.EventStore.GetEventStore.Tests.v2.ncrunchproject index 8782fa034e619e9f43e86fcb6dd356a536eaf6c8..724068ebc7d28c10e685b4ba89a7a50161aeaa68 100644 GIT binary patch delta 11 Scmca6{zY`dF|Nrgxb*-bjRfle delta 228 zcmew&dQE)8F|NtyIN9nQ8B%~Slc5BN^BM9O5*czB+<@#HhE#@Ph7h2bCr~sQ$S(z| zOJ%TQ&;!C4h613XYy>nEWIf0sAcJBUw17|#h;1138Ax*G + { + var streamId = string.Concat("stream-", iteration); + await eventStore + .AppendToStream(streamId, ExpectedVersion.Any, new NewStreamEvent(Guid.NewGuid(), "type", "\"data\"", "\"metadata\"")) + .MightThrow("Append failed due to WrongExpectedVersion. Stream: {0}, Expected version: -2".FormatWith(streamId)); + }); + + } + } + } + } +} diff --git a/src/Cedar.EventStore.Postgres.Tests/TaskExtensions.cs b/src/Cedar.EventStore.Postgres.Tests/TaskExtensions.cs new file mode 100644 index 000000000..8077379cf --- /dev/null +++ b/src/Cedar.EventStore.Postgres.Tests/TaskExtensions.cs @@ -0,0 +1,26 @@ +namespace Cedar.EventStore.Postgres.Tests +{ + using FluentAssertions; + using System; + using System.Threading.Tasks; + + internal static class TaskExtensions + { + internal static async Task MightThrow(this Task task, string message) + { + try + { + await task; + } + catch (Exception ex) + { + ex.Should().BeOfType(); + ex.Message.Should().Be(message); + + return; + } + + //it didn't throw an exception, that's ok too + } + } +} diff --git a/src/nuget.config b/src/nuget.config index aa433be65..183217403 100644 --- a/src/nuget.config +++ b/src/nuget.config @@ -1,6 +1,3 @@ - - - - - + + \ No newline at end of file From 2854035942f71e557ab20b8f626f1c1c2bf63a7f Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Thu, 8 Oct 2015 15:40:09 +0100 Subject: [PATCH 30/34] Remove errant writer.close() --- src/Cedar.EventStore.Postgres/PostgresEventStore.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs index ebfbbe2a0..bea91eda2 100644 --- a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -182,8 +182,7 @@ public async Task AppendToStream( writer.Write(@event.JsonData, NpgsqlDbType.Json); writer.Write(@event.JsonMetadata, NpgsqlDbType.Json); } - - writer.Close(); + tx.Commit(); } } From f1a958adae90d676b9dc2b43494d06f50dc166a4 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Thu, 8 Oct 2015 15:53:10 +0100 Subject: [PATCH 31/34] Move tx.commit() outside using() binaryImport block --- src/Cedar.EventStore.Postgres/PostgresEventStore.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs index bea91eda2..aa95c556b 100644 --- a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -182,9 +182,9 @@ public async Task AppendToStream( writer.Write(@event.JsonData, NpgsqlDbType.Json); writer.Write(@event.JsonMetadata, NpgsqlDbType.Json); } - - tx.Commit(); - } + } + + tx.Commit(); } catch (NpgsqlException ex) { From 176fb9f1fba6a6d48866748e84be71698ed68eb8 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Mon, 9 Nov 2015 14:58:57 +0000 Subject: [PATCH 32/34] Add try/catch for concurrency when creating a stream --- .../PostgresEventStore.cs | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs index aa95c556b..db0c01f8e 100644 --- a/src/Cedar.EventStore.Postgres/PostgresEventStore.cs +++ b/src/Cedar.EventStore.Postgres/PostgresEventStore.cs @@ -141,7 +141,9 @@ public async Task AppendToStream( { // create the stream as it doesn't exist - using ( + try + { + using ( var command = new NpgsqlCommand(_scripts.Functions.CreateStream, connection, tx) { CommandType @@ -149,12 +151,27 @@ public async Task AppendToStream( CommandType .StoredProcedure }) + { + command.Parameters.AddWithValue(":stream_id", streamIdInfo.StreamId); + command.Parameters.AddWithValue(":stream_id_original", streamIdInfo.StreamIdOriginal); + + streamIdInternal = + (int)await command.ExecuteScalarAsync(cancellationToken).NotOnCapturedContext(); + } + } + catch (NpgsqlException ex) { - command.Parameters.AddWithValue(":stream_id", streamIdInfo.StreamId); - command.Parameters.AddWithValue(":stream_id_original", streamIdInfo.StreamIdOriginal); + if (ex.Code == "40001") + { + // could not serialize access due to read/write dependencies among transactions + throw new WrongExpectedVersionException( + Messages.AppendFailedWrongExpectedVersion.FormatWith(streamId, expectedVersion), ex); + } - streamIdInternal = - (int)await command.ExecuteScalarAsync(cancellationToken).NotOnCapturedContext(); + //if error code is 40001 the transaction is already rolled back + tx.Rollback(); + + throw; } } } From c1f9e653033a1152537b818ab045cbac6c1d6654 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Mon, 9 Nov 2015 14:59:35 +0000 Subject: [PATCH 33/34] include concurrency tests --- .../Cedar.EventStore.Postgres.Tests.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.csproj b/src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.csproj index af3c893a4..9b0869488 100644 --- a/src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.csproj +++ b/src/Cedar.EventStore.Postgres.Tests/Cedar.EventStore.Postgres.Tests.csproj @@ -61,10 +61,12 @@ + + From e4649ccd221d10766ba27e432b5ed4b8cae56455 Mon Sep 17 00:00:00 2001 From: Dan Barua Date: Mon, 9 Nov 2015 14:59:58 +0000 Subject: [PATCH 34/34] Fix #3 Use Npgsql 3.0.3 stable --- .../Cedar.EventStore.Postgres.csproj | 4 ++-- src/Cedar.EventStore.Postgres/packages.config | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj index 5615b72e5..f0077a787 100644 --- a/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj +++ b/src/Cedar.EventStore.Postgres/Cedar.EventStore.Postgres.csproj @@ -35,8 +35,8 @@ ..\packages\Ensure.That.2.0.0\lib\portable-net4+sl5+netcore45+wpa81+wp8+MonoAndroid1+MonoTouch1\EnsureThat.dll\EnsureThat.dll True - - ..\packages\Npgsql.3.1.0-unstable0000\lib\net45\Npgsql.dll + + ..\packages\Npgsql.3.0.3\lib\net45\Npgsql.dll True diff --git a/src/Cedar.EventStore.Postgres/packages.config b/src/Cedar.EventStore.Postgres/packages.config index aa85ed273..9d1a043b2 100644 --- a/src/Cedar.EventStore.Postgres/packages.config +++ b/src/Cedar.EventStore.Postgres/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file