[
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file\n\nversion: 2\nupdates:\n# eventstore\n  - package-ecosystem: \"gomod\"\n    directory: \"/eventstore/kurrent/\"\n    schedule:\n      interval: \"weekly\"\n  - package-ecosystem: \"gomod\"\n    directory: \"/eventstore/esdb/\"\n    schedule:\n      interval: \"weekly\"\n  - package-ecosystem: \"gomod\"\n    directory: \"/eventstore/bbolt/\"\n    schedule:\n      interval: \"weekly\"\n  - package-ecosystem: \"gomod\" \n    directory: \"/eventstore/sql/\" \n    schedule:\n      interval: \"weekly\"\n# snapshotstore\n  - package-ecosystem: \"gomod\"\n    directory: \"/snapshotstore/sql/\"\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/go.yml",
    "content": "# This workflow will build a golang project\n# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go\n\nname: Go\n\npermissions:\n  contents: read\n\non:\n  push:\n    branches: [ \"master\" ]\n  pull_request:\n    branches: [ \"master\" ]\n\njobs:\n\n  main:\n    name: Main module\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: '1.19'\n\n      - name: Build\n        run: go build -v ./...\n\n      - name: Test\n        run: go test -v -race ./...\n\n  bbolt:\n    name: bbolt eventstore\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: '1.21'\n\n      - name: Build\n        run: cd eventstore/bbolt && go build -v ./...\n\n      - name: Test\n        run: cd eventstore/bbolt && go test -v -race ./...\n       \n  sql:\n    name: sql eventstore\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: '1.23'\n\n      - name: Build\n        run: cd eventstore/sql && go build -v ./...\n\n      - name: Test\n        run: cd eventstore/sql && go test -v -race ./...\n\n  esdb:\n    name: esdb eventstore\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: '1.21'\n\n      - name: Build\n        run: cd eventstore/esdb && go build -v ./...\n\n      - name: Test\n        run: cd eventstore/esdb && go test -v -race ./...\n\n  kurrent:\n    name: kurrent eventstore\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: '1.21'\n\n      - name: Build\n        run: cd eventstore/kurrent && go build -v ./...\n\n      - name: Test\n        run: cd eventstore/kurrent && go test -v -race ./...\n\n  sqlsnapshot:\n    name: sql snapshotstore\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: '1.23'\n\n      - name: Build\n        run: cd snapshotstore/sql && go build -v ./...\n\n      - name: Test\n        run: cd snapshotstore/sql && go test -v -race ./...\n"
  },
  {
    "path": ".gitignore",
    "content": "example/example\n.idea\ncoverage.txt\n*.swp\ngo.work\n"
  },
  {
    "path": "LICENSE",
    "content": "__________________________________\n\nMozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n    means each individual or legal entity that creates, contributes to\n    the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n    means the combination of the Contributions of others (if any) used\n    by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n    means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n    means Source Code Form to which the initial Contributor has attached\n    the notice in Exhibit A, the Executable Form of such Source Code\n    Form, and Modifications of such Source Code Form, in each case\n    including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n    means\n\n    (a) that the initial Contributor has attached the notice described\n        in Exhibit B to the Covered Software; or\n\n    (b) that the Covered Software was made available under the terms of\n        version 1.1 or earlier of the License, but not also under the\n        terms of a Secondary License.\n\n1.6. \"Executable Form\"\n    means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n    means a work that combines Covered Software with other material, in\n    a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n    means this document.\n\n1.9. \"Licensable\"\n    means having the right to grant, to the maximum extent possible,\n    whether at the time of the initial grant or subsequently, any and\n    all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n    means any of the following:\n\n    (a) any file in Source Code Form that results from an addition to,\n        deletion from, or modification of the contents of Covered\n        Software; or\n\n    (b) any new file in Source Code Form that contains any Covered\n        Software.\n\n1.11. \"Patent Claims\" of a Contributor\n    means any patent claim(s), including without limitation, method,\n    process, and apparatus claims, in any patent Licensable by such\n    Contributor that would be infringed, but for the grant of the\n    License, by the making, using, selling, offering for sale, having\n    made, import, or transfer of either its Contributions or its\n    Contributor Version.\n\n1.12. \"Secondary License\"\n    means either the GNU General Public License, Version 2.0, the GNU\n    Lesser General Public License, Version 2.1, the GNU Affero General\n    Public License, Version 3.0, or any later versions of those\n    licenses.\n\n1.13. \"Source Code Form\"\n    means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n    means an individual or a legal entity exercising rights under this\n    License. For legal entities, \"You\" includes any entity that\n    controls, is controlled by, or is under common control with You. For\n    purposes of this definition, \"control\" means (a) the power, direct\n    or indirect, to cause the direction or management of such entity,\n    whether by contract or otherwise, or (b) ownership of more than\n    fifty percent (50%) of the outstanding shares or beneficial\n    ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n    Licensable by such Contributor to use, reproduce, make available,\n    modify, display, perform, distribute, and otherwise exploit its\n    Contributions, either on an unmodified basis, with Modifications, or\n    as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n    for sale, have made, import, and otherwise transfer either its\n    Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n    or\n\n(b) for infringements caused by: (i) Your and any other third party's\n    modifications of Covered Software, or (ii) the combination of its\n    Contributions with other software (except as part of its Contributor\n    Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n    its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n    Form, as described in Section 3.1, and You must inform recipients of\n    the Executable Form how they can obtain a copy of such Source Code\n    Form by reasonable means in a timely manner, at a charge no more\n    than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n    License, or sublicense it under different terms, provided that the\n    license for the Executable Form does not attempt to limit or alter\n    the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n*                                                                      *\n*  6. Disclaimer of Warranty                                           *\n*  -------------------------                                           *\n*                                                                      *\n*  Covered Software is provided under this License on an \"as is\"       *\n*  basis, without warranty of any kind, either expressed, implied, or  *\n*  statutory, including, without limitation, warranties that the       *\n*  Covered Software is free of defects, merchantable, fit for a        *\n*  particular purpose or non-infringing. The entire risk as to the     *\n*  quality and performance of the Covered Software is with You.        *\n*  Should any Covered Software prove defective in any respect, You     *\n*  (not any Contributor) assume the cost of any necessary servicing,   *\n*  repair, or correction. This disclaimer of warranty constitutes an   *\n*  essential part of this License. No use of any Covered Software is   *\n*  authorized under this License except under this disclaimer.         *\n*                                                                      *\n************************************************************************\n\n************************************************************************\n*                                                                      *\n*  7. Limitation of Liability                                          *\n*  --------------------------                                          *\n*                                                                      *\n*  Under no circumstances and under no legal theory, whether tort      *\n*  (including negligence), contract, or otherwise, shall any           *\n*  Contributor, or anyone who distributes Covered Software as          *\n*  permitted above, be liable to You for any direct, indirect,         *\n*  special, incidental, or consequential damages of any character      *\n*  including, without limitation, damages for lost profits, loss of    *\n*  goodwill, work stoppage, computer failure or malfunction, or any    *\n*  and all other commercial damages or losses, even if such party      *\n*  shall have been informed of the possibility of such damages. This   *\n*  limitation of liability shall not apply to liability for death or   *\n*  personal injury resulting from such party's negligence to the       *\n*  extent applicable law prohibits such limitation. Some               *\n*  jurisdictions do not allow the exclusion or limitation of           *\n*  incidental or consequential damages, so this exclusion and          *\n*  limitation may not apply to You.                                    *\n*                                                                      *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n  This Source Code Form is subject to the terms of the Mozilla Public\n  License, v. 2.0. If a copy of the MPL was not distributed with this\n  file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n  This Source Code Form is \"Incompatible With Secondary Licenses\", as\n  defined by the Mozilla Public License, v. 2.0.\n"
  },
  {
    "path": "Makefile",
    "content": "all:\n\tgo fmt ./...\n\tgo build\n\n\t#core\n\tcd core && go build\n\t# event stores\n\tcd eventstore/bbolt && go build\n\tcd eventstore/sql && go build\n\tcd eventstore/esdb && go build\ntest:\n\t#core\n\tcd core && go test -count 1 ./...\n\t# event stores\n\tcd eventstore/bbolt && go test -count 1 ./...\n\tcd eventstore/sql && go test -count 1 ./...\n\tcd eventstore/esdb && go test esdb_test.go -count 1 ./...\n\n\t# main\n\tgo test -count 1 ./...\nupdate:\n\t# event stores\n\tcd eventstore/bbolt && go get -t -u ./... && go mod tidy\n\tcd eventstore/sql && go get -u ./... && go mod tidy\n\tcd eventstore/esdb && go get -t -u ./... && go mod tidy\n\n\t#snaptshot stores\n\tcd snapshotstore/sql && go get -u ./... && go mod tidy\n \n\t# main\n\tgo get -t -u ./... && go mod tidy\n"
  },
  {
    "path": "README.md",
    "content": "# Overview\n\nThis set of modules is a post implementation of [@jen20's](https://github.com/jen20) way of implementing event sourcing. You can find the original blog post [here](https://jen20.dev/post/event-sourcing-in-go/) and github repo [here](https://github.com/jen20/go-event-sourcing-sample).\n\nIt's structured in two main parts:\n\n* [Aggregate](https://github.com/hallgren/eventsourcing?tab=readme-ov-file#aggregate) - Model and Load/Save aggregates (write side).\n* [Consuming events](https://github.com/hallgren/eventsourcing?tab=readme-ov-file#projections) - Handle events and build read-models (read side).\n\n## Event Sourcing\n\n[Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html) is a technique to make it possible to capture all changes to an application state as a sequence of events.\n\n### Aggregate\n\nThe *aggregate* is the central point where events are bound. The aggregate struct needs to embed `aggregate.Root` to get the aggregate behaviors.\n\n*Person* aggregate where the Aggregate Root is embedded next to the `Name` and `Age` properties.\n\n```go\ntype Person struct {\n\taggregate.Root\n\tName string\n\tAge  int\n}\n```\n\nThe aggregate needs to implement the `Transition(event eventsourcing.Event)` and `Register(r aggregate.RegisterFunc)` methods to fulfill the aggregate interface.\nThese methods define how events are transformed to build the aggregate state and which events to register into the repository.\n\nExample of the Transition method on the `Person` aggregate.\n\n```go\n// Transition the person state dependent on the events\nfunc (person *Person) Transition(event eventsourcing.Event) {\n    switch e := event.Data().(type) {\n    case *Born:\n            person.Age = 0\n            person.Name = e.Name\n    case *AgedOneYear:\n            person.Age += 1\n    }\n}\n```\n\nThe `Born` event sets the `Person` property `Age` and `Name`, and the `AgedOneYear` adds one year to the `Age` property. This makes the state of the aggregate flexible and could easily change in the future if required.\n\nExample or the Register method:\n\n```go\n// Register callback method that register Person events to the repository\nfunc (person *Person) Register(r aggregate.RegisterFunc) {\n    r(&Born{}, &AgedOneYear{})\n}\n```\n\nThe `Born` and `AgedOneYear` events are now registered to the repository when the aggregate is registered.\n\n### Event\n\nAn event is a clean struct with exported properties that contains the state of the event.\n\nExample of two events from the `Person` aggregate.\n\n```go\n// Initial event\ntype Born struct {\n    Name string\n}\n\n// Event that happens once a year\ntype AgedOneYear struct {}\n```\n\nWhen an aggregate is first created, an event is needed to initialize the state of the aggregate. No event, no aggregate.\nExample of a constructor that returns the `Person` aggregate and inside it binds an event via the `aggregate.TrackChange` function.\nIt's possible to define rules that the aggregate must uphold before an event is created, in this case the person's name must not be blank.\n\n```go\n// CreatePerson constructor for Person\nfunc CreatePerson(name string) (*Person, error) {\n\tif name == \"\" {\n\t\treturn nil, errors.New(\"name can't be blank\")\n\t}\n\tperson := Person{}\n\taggregate.TrackChange(&person, &Born{Name: name})\n\treturn &person, nil\n}\n```\n\nWhen a person is created, more events could be created via methods on the `Person` aggregate. Below is the `GrowOlder` method which in turn triggers the event `AgedOneYear`.\n\n```go\n// GrowOlder command\nfunc (person *Person) GrowOlder() {\n\taggregate.TrackChange(person, &AgedOneYear{})\n}\n```\n\nInternally the `aggregate.TrackChange` function calls the `Transition` method on the aggregate to transform the aggregate based on the newly created event.\n\nTo bind metadata to events use the `aggregate.TrackChangeWithMetadata` function.\n  \nThe `Event` has the following behaviours..\n\n```go\ntype Event struct {\n    // aggregate identifier \n    AggregateID() string\n    // the aggregate version when this event was created\n    Version() Version\n    // the global version is based on all events (this value is only set after the event is saved to the event store) \n    GlobalVersion() Version\n    // aggregate type (Person in the example above)\n    AggregateType() string\n    // UTC time when the event was created  \n    Timestamp() time.Time\n    // the specific event data specified in the application (Born{}, AgedOneYear{})\n    Data() interface{}\n    // data that don´t belongs to the application state (could be correlation id or other request references)\n    Metadata() map[string]interface{}\n}\n```\n\n### Aggregate ID\n\nThe identifier on the aggregate is default set by a random generated string via the crypt/rand pkg. It is possible to change the default behavior in two ways.\n\n* Set a specific id on the aggregate via the SetID func.\n\n```go\nvar id = \"123\"\nperson := Person{}\nerr := person.SetID(id)\n```\n\n* Change the id generator via the global aggregate.SetIDFunc function.\n\n```go\nvar counter = 0\nf := func() string {\n\tcounter++\n\treturn fmt.Sprint(counter)\n}\n\naggregate.SetIDFunc(f)\n```\n\n## Save/Load Aggregate\n\nTo save and load aggregates there are exported functions on the aggregate package. `core.EventStore` is an interface exposing the actual storage system. More on that in later sections.\nThe `aggregate` interface is automatically applied on the application defined aggregate when `aggregate.Root` is embedded. \n\n```go\n// Save stores the aggregate events in the supplied event store\naggregate.Save(es core.EventStore, a aggregate) error \n\n// Load returns the aggregate based on its events\naggregate.Load(ctx context.Context, es core.EventStore, id string, a aggregate) error\n```\n\nTo be able to save and load aggregates they have to be registered and each aggregate has to implement the `Register` method. On top of that the aggregate itself has to be registered via\nthe `aggregate.Register` function.\n\n```go\n// the person aggregate has to be registered in the repository\naggregate.Register(&Person{})\n```\n\n### Event Store\n\nThe only thing an event store handles are events, and it must implement the following interface.\n\n```go\n// saves events to the underlaying data store.\nSave(events []core.Event) error\n\n// fetches events based on identifier and type but also after a specific version. The version is used to load events that happened after a snapshot was taken.\nGet(id string, aggregateType string, afterVersion core.Version) (core.Iterator, error)\n```\n\nThere are four implementations in this repository.\n\n* [SQL](https://github.com/hallgren/eventsourcing/blob/master/eventstore/sql/README.md) - `go get github.com/hallgren/eventsourcing/eventstore/sql`\n\t* SQLite\n\t* Postgres\n \t* Microsoft SQL Server \n* Bolt - `go get github.com/hallgren/eventsourcing/eventstore/bbolt`\n* Event Store DB - `go get github.com/hallgren/eventsourcing/eventstore/esdb`\n* Kurrent DB - `go get github.com/hallgren/eventsourcing/eventstore/kurrent`\n* RAM Memory - part of the main module\n\nExternal event stores:\n\n* [DynamoDB](https://github.com/fd1az/dynamo-es) by [fd1az](https://github.com/fd1az)\n* [SQL pgx driver](https://github.com/CentralConcept/go-eventsourcing-pgx/tree/main/eventstore/pgx)\n\n### Custom event store\n\nIf you want to store events in a database beside the already implemented event stores you can implement, or provide, another event store. It has to implement the `core.EventStore` \ninterface to support the eventsourcing.EventRepository.\n\n```go\ntype EventStore interface {\n    Save(events []core.Event) error\n    Get(id string, aggregateType string, afterVersion core.Version) (core.Iterator, error)\n}\n```\n\nThe event store needs to import the `github.com/hallgren/eventsourcing/core` module that expose the `core.Event`, `core.Version` and `core.Iterator` types.\n\n### Encoder\n\nBefore an `eventsourcing.Event` is stored into a event store it has to be transformed into an `core.Event`. This is done with an encoder that serializes the data properties `Data` and `Metadata` into `[]byte`.\nWhen an event is fetched the encoder deserialize the `Data` and `Metadata` `[]byte` back into their actual types.\n\nThe default encoder uses the `encoding/json` package for serialization/deserialization. It can be replaced by using the `eventsourcing.SetEventEncoder(e Encoder)` function on the eventsourcing  package, it has to follow this interface:\n\n```go\ntype Encoder interface {\n\tSerialize(v interface{}) ([]byte, error)\n\tDeserialize(data []byte, v interface{}) error\n}\n```\n\n### Realtime Event Subscription\n\nFor now the real time event subscription has been removed as I'm not satisfied with the exported API. Please fill an issue if you want it back.\n\n## Snapshot\n\nIf an aggregate has a lot of events it can take some time fetching and building the aggregate. This can be optimized with the help of a snapshot.\nThe snapshot is the state of the aggregate on a specific version. Instead of iterating all events, only the events after the version are iterated and\nused to build the aggregate. The use of snapshots is optional and is exposed via the snapshot functions on the aggregate package.\n\n### Save/Load Snapshot \n\nIt's only possible to save a snapshot if it has no pending events, meaning that the aggregate events are saved before saving the snapshot.\n\n```go\n// Saves a snapshot\naggregate.SaveSnapshot(ss core.SnapshotStore, s snapshot) error\n\n// Loads the aggregate only from the snapshot state not adding events that were saved after the snapshot was taken\naggregate.LoadSnapshot(ctx context.Context, ss core.SnapshotStore, id string, s snapshot) error\n\n// Loads the aggregate from the snapshot and also adds events\naggregate.LoadFromSnapshot(ctx context.Context, es core.EventStore, ss core.SnapshotStore, id string, as aggregateSnapshot) error\n```\n\n### Snapshot Store\n\nLike the event store's the snapshot repository is built on the same design. The snapshot store has to implement the following methods.\n\n```go\ntype SnapshotStore interface {\n\tSave(snapshot Snapshot) error\n\tGet(ctx context.Context, id, aggregateType string) (Snapshot, error)\n}\n```\n\nThere are two implementations in this repository.\n\n* [SQL](https://github.com/hallgren/eventsourcing/blob/master/snapshotstore/sql/README.md) - `go get github.com/hallgren/eventsourcing/snapshotstore/sql`\n\t* SQLite\n\t* Postgres\n* RAM Memory - part of the main module\n\nExternal event stores:\n\n* [SQL pgx driver](https://github.com/CentralConcept/go-eventsourcing-pgx/tree/main/snapshotstore/pgx)\n\n### Unexported aggregate properties\n\nAs unexported properties on a struct are not possible to serialize there is the same limitation on aggregates.\nTo fix this there are optional callback methods that can be added to the aggregate struct.\n\n```go\ntype snapshot interface {\n\tSerializeSnapshot(SerializeFunc) ([]byte, error)\n\tDeserializeSnapshot(DeserializeFunc, []byte) error\n}\n```\n\nExample:\n\n```go\n// aggregate\ntype Person struct {\n\taggregate.Root\n\tunexported string\n}\n\n// snapshot struct\ntype PersonSnapshot struct {\n\tUnExported string\n}\n\n// callback that maps the aggregate to the snapshot struct with the exported property\nfunc (s *Person) SerializeSnapshot(m aggregate.SnapshotMarshal) ([]byte, error) {\n\tsnap := PersonSnapshot{\n\t\tUnexported: s.unexported,\n\t}\n\treturn m(snap)\n}\n\n// callback to map the snapshot back to the aggregate\nfunc (s *Person) DeserializeSnapshot(m aggregate.SnapshotUnmarshal, b []byte) error {\n\tsnap := PersonSnapshot{}\n\terr := m(b, &snap)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.unexported = snap.UnExported\n\treturn nil\n}\n```\n\nIt's possible to change the default json encoder by the `eventsourcing.SetSnapshotEncoder(e Encoder)` function.\n\n## Projections\n\nProjections is a way to build read-models based on events. A read-model is a way to expose data from events in a different form. Where the form is optimized for read-only queries.\n\nIf you want more background on projections check out Derek Comartin projections article [Projections in Event Sourcing: Build ANY model you want!](https://codeopinion.com/projections-in-event-sourcing-build-any-model-you-want/) or Martin Fowler's [CQRS](https://martinfowler.com/bliki/CQRS.html).\n\n### Projection\n\nA _projection_ is created from the `eventsourcing.NewProjection` function. The method takes a `core.Fetcher` and a `callbackFunc` and returns a pointer to the projection.\n\n```go\np := pr.Projection(f core.Fetcher, c callbackFunc)\n```\n\nThe core.Fetcher type `func() (core.Iterator, error)`, i.e it return the same signature that event stores return when they return events.\n\n```go\ntype Fetcher func() (core.Iterator, error)\n```\n\nThe `callbackFunc` is called for every iterated event inside the projection. The event is typed and can be handled in the same way as the aggregate `Transition()` method.\n\n```go\ntype callbackFunc func(e eventsourcing.Event) error\n```\n\nExample: Creates a projection that fetches all events from an event store and handle them in the callbackF.\n\n```go\np := eventsourcing.NewProjection(es.All(0, 1), func(event eventsourcing.Event) error {\n\tswitch e := event.Data().(type) {\n\tcase *Born:\n\t\t// handle the event\n\t}\n\treturn nil\n})\n```\n\n### Projection execution\n\nA projection can be started in three different ways.\n\n#### RunOnce\n\nRunOnce fetch events from the event store one time. It returns true if there were events to iterate otherwise false.\n\n```go\nRunOnce() (bool, ProjectionResult)\n```\n\n#### RunToEnd\n\nRunToEnd fetches events from the event store until it reaches the end of the event stream. A context is passed in making it possible to cancel the projections from the outside.\n\n```go\nRunToEnd(ctx context.Context) ProjectionResult\n```\n\n`RunOnce` and `RunToEnd` both return a ProjectionResult \n\n```go\ntype ProjectionResult struct {\n\tError          \t\terror\n\tProjectionName \t\tstring\n\tLastHandledEvent\tEvent\n}\n```\n\n* **Error** Is set if the projection returned an error\n* **ProjectionName** Is the name of the projection\n* **LastHandledEvent** The last successfully handled event (can be useful during debugging)\n\n#### Run\n\nRun will run forever until the event consumer is returning an error or if it's canceled from the outside. When it hits the end of the event stream it will start a timer and sleep the time set in the projection property `Pace`.\n\n ```go\n Run(ctx context.Context, pace time.Duration) error\n ```\n\nA running projection can be triggered manually via `TriggerAsync()` or `TriggerSync()`.\n\n### Projection properties\n\nA projection has a set of properties that can affect its behavior.\n\n* **Strict** - Default true and it will trigger an error if a fetched event is not registered in the event `Register`. This forces all events to be handled by the callbackFunc.\n* **Name** - The name of the projection. Can be useful when debugging multiple running projections. The default name is the index it was created from the projection handler.\n\n### Run multiple projections\n\n#### Group \n\nA set of projections can run concurrently in a group.\n\n```go\ng := eventsourcing.NewProjectionGroup(p1, p2, p3)\n```\n\nA group is started with `g.Start()` where each projection will run in a separate go routine. Errors from a projection can be retrieved from an error channel `g.ErrChan`.\n\nThe `g.Stop()` method is used to halt all projections in the group and it returns when all projections have stopped.\n\n```go\n// create three projections\np1 := eventsourcing.NewProjection(es.All(0, 1), callbackF)\np2 := eventsourcing.NewProjection(es.All(0, 1), callbackF)\np3 := eventsourcing.NewProjection(es.All(0, 1), callbackF)\n\n// create a group containing the projections\ng := eventsourcing.NewProjectionGroup(p1, p2, p3)\n\n// Start runs all projections concurrently\ng.Start()\n\n// Stop terminate all projections and wait for them to return\ndefer g.Stop()\n\n// handling error in projection or termination from outside\nselect {\n\tcase err := <-g.ErrChan:\n\t\t// handle the error\n\tcase <-doneChan:\n\t\t// stop signal from the out side\n\t\treturn\n}\n```\n\nThe pace of the projection can be changed with the `Pace` property. Default is every 10 seconds.\n\nIf the pace is not fast enough for some scenario it's possible to trigger manually.\n\n`TriggerAsync()`: Triggers all projections in the group and returns.\n\n`TriggerSync()`: Triggers all projections in the group and waits for them running to the end of their event streams.\n\n#### Race\n\nCompared to a group the race is a one shot operation. Instead of fetching events continuously it's used to iterate and process all existing events and then return.\n\nThe `Race()` method starts the projections and runs them to the end of their event streams. When all projections are finished the method returns.\n\n```go\neventsourcing.ProjectionsRace(cancelOnError bool, projections ...*Projection) ([]ProjectionResult, error)\n```\n\nIf `cancelOnError` is set to true the method will halt all projections and return if any projection is returning an error.\n\nThe returned `[]ProjectionResult` is a collection of all projection results.\n\nRace example:\n\n```go\n// create two projections\np1 := eventsourcing.NewProjection(es.All(0, 1), callbackF)\np2 := eventsourcing.NewProjection(es.All(0, 1), callbackF)\n\n// true make the race return on error in any projection\nresult, err := eventsourcing.ProjectionsRace(true, r1, r2)\n```\n"
  },
  {
    "path": "aggregate/aggregate.go",
    "content": "package aggregate\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\n\t\"github.com/hallgren/eventsourcing\"\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/internal\"\n)\n\ntype RegisterFunc = func(events ...interface{})\n\n// Aggregate interface to use the aggregate root specific methods\ntype aggregate interface {\n\troot() *Root\n\tTransition(event eventsourcing.Event)\n\tRegister(RegisterFunc)\n}\n\n// Load returns the aggregate based on its events\nfunc Load(ctx context.Context, es core.EventStore, id string, a aggregate) error {\n\tif reflect.ValueOf(a).Kind() != reflect.Ptr {\n\t\treturn eventsourcing.ErrAggregateNeedsToBeAPointer\n\t}\n\n\troot := a.root()\n\n\titerator, err := getEvents(ctx, es, id, aggregateType(a), root.Version())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer iterator.Close()\n\tfor iterator.Next() {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t\tevent, err := iterator.Value()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tbuildFromHistory(a, []eventsourcing.Event{event})\n\t\t}\n\t}\n\tif root.Version() == 0 {\n\t\treturn eventsourcing.ErrAggregateNotFound\n\t}\n\treturn nil\n}\n\n// LoadFromSnapshot fetch the aggregate by first get its snapshot and later append events after the snapshot was stored\n// This can speed up the load time of aggregates with many events\nfunc LoadFromSnapshot(ctx context.Context, es core.EventStore, ss core.SnapshotStore, id string, as aggregateSnapshot) error {\n\terr := LoadSnapshot(ctx, ss, id, as)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn Load(ctx, es, id, as)\n}\n\n// Save stores the aggregate events in the supplied event store\nfunc Save(es core.EventStore, a aggregate) error {\n\troot := a.root()\n\n\t// return as quick as possible when no events to process\n\tif len(root.events) == 0 {\n\t\treturn nil\n\t}\n\n\tif !internal.GlobalRegister.AggregateRegistered(a) {\n\t\treturn fmt.Errorf(\"%s %w\", aggregateType(a), eventsourcing.ErrAggregateNotRegistered)\n\t}\n\n\tglobalVersion, err := saveEvents(es, root.Events())\n\tif err != nil {\n\t\treturn err\n\t}\n\t// update the global version on the aggregate\n\troot.globalVersion = globalVersion\n\n\t// set internal properties and reset the events slice\n\tlastEvent := root.events[len(root.events)-1]\n\troot.version = lastEvent.Version()\n\troot.events = []eventsourcing.Event{}\n\n\treturn nil\n}\n\n// Register registers the aggregate and its events\nfunc Register(a aggregate) {\n\tinternal.GlobalRegister.Register(a)\n}\n\n// Save events to the event store\nfunc saveEvents(eventStore core.EventStore, events []eventsourcing.Event) (eventsourcing.Version, error) {\n\tvar esEvents = make([]core.Event, 0, len(events))\n\n\tfor _, event := range events {\n\t\tdata, err := internal.EventEncoder.Serialize(event.Data())\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tmetadata, err := internal.EventEncoder.Serialize(event.Metadata())\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tesEvent := core.Event{\n\t\t\tAggregateID:   event.AggregateID(),\n\t\t\tVersion:       core.Version(event.Version()),\n\t\t\tAggregateType: event.AggregateType(),\n\t\t\tTimestamp:     event.Timestamp(),\n\t\t\tData:          data,\n\t\t\tMetadata:      metadata,\n\t\t\tReason:        event.Reason(),\n\t\t}\n\t\t_, ok := internal.GlobalRegister.EventRegistered(esEvent)\n\t\tif !ok {\n\t\t\treturn 0, fmt.Errorf(\"%s %w\", esEvent.Reason, eventsourcing.ErrEventNotRegistered)\n\t\t}\n\t\tesEvents = append(esEvents, esEvent)\n\t}\n\n\terr := eventStore.Save(esEvents)\n\tif err != nil {\n\t\tif errors.Is(err, core.ErrConcurrency) {\n\t\t\treturn 0, eventsourcing.ErrConcurrency\n\t\t}\n\t\treturn 0, fmt.Errorf(\"error from event store: %w\", err)\n\t}\n\n\treturn eventsourcing.Version(esEvents[len(esEvents)-1].GlobalVersion), nil\n}\n\n// getEvents return event iterator based on aggregate inputs from the event store\nfunc getEvents(ctx context.Context, eventStore core.EventStore, id, aggregateType string, fromVersion eventsourcing.Version) (*eventsourcing.Iterator, error) {\n\t// fetch events after the current version of the aggregate that could be fetched from the snapshot store\n\teventIterator, err := eventStore.Get(ctx, id, aggregateType, core.Version(fromVersion))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &eventsourcing.Iterator{\n\t\tCoreIterator: eventIterator,\n\t}, nil\n}\n"
  },
  {
    "path": "aggregate/aggregate_test.go",
    "content": "package aggregate_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing\"\n\t\"github.com/hallgren/eventsourcing/aggregate\"\n\t\"github.com/hallgren/eventsourcing/eventstore/memory\"\n\tss \"github.com/hallgren/eventsourcing/snapshotstore/memory\"\n)\n\nfunc TestSaveAndLoadAggregate(t *testing.T) {\n\tes := memory.Create()\n\taggregate.Register(&Person{})\n\n\tperson, err := CreatePerson(\"kalle\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = aggregate.Save(es, person)\n\tif err != nil {\n\t\tt.Fatalf(\"could not save aggregate, err: %v\", err)\n\t}\n\n\t// make sure the global version is set to 1\n\tif person.GlobalVersion() != 1 {\n\t\tt.Fatalf(\"global version is: %d expected: 1\", person.GlobalVersion())\n\t}\n\n\ttwin := Person{}\n\terr = aggregate.Load(context.Background(), es, person.ID(), &twin)\n\tif err != nil {\n\t\tt.Fatalf(\"could not get aggregate err: %v\", err)\n\t}\n\n\t// Check internal aggregate version\n\tif person.Version() != twin.Version() {\n\t\tt.Fatalf(\"Wrong version org %q copy %q\", person.Version(), twin.Version())\n\t}\n\n\t// Check person Name\n\tif person.Name != twin.Name {\n\t\tt.Fatalf(\"Wrong Name org %q copy %q\", person.Name, twin.Name)\n\t}\n}\n\nfunc TestLoadAggregateFromSnapshot(t *testing.T) {\n\tes := memory.Create()\n\tss := ss.Create()\n\taggregate.Register(&Person{})\n\n\tperson, err := CreatePerson(\"kalle\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = aggregate.Save(es, person)\n\tif err != nil {\n\t\tt.Fatalf(\"could not save aggregate, err: %v\", err)\n\t}\n\n\t// store snapshot\n\terr = aggregate.SaveSnapshot(ss, person)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// add one more event to the person aggregate\n\tperson.GrowOlder()\n\terr = aggregate.Save(es, person)\n\n\t// load person to person2 from snaphost and events\n\tperson2 := &Person{}\n\terr = aggregate.LoadFromSnapshot(context.Background(), es, ss, person.ID(), person2)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif person.Age != person2.Age {\n\t\tt.Fatalf(\"expected same age on person(%d) and person2(%d)\", person.Age, person2.Age)\n\t}\n}\n\nfunc TestLoadNoneExistingAggregate(t *testing.T) {\n\tes := memory.Create()\n\taggregate.Register(&Person{})\n\n\tp := Person{}\n\terr := aggregate.Load(context.Background(), es, \"none_existing\", &p)\n\tif err != eventsourcing.ErrAggregateNotFound {\n\t\tt.Fatal(\"could not get aggregate\")\n\t}\n}\n"
  },
  {
    "path": "aggregate/idgenerator.go",
    "content": "package aggregate\n\nimport (\n\t\"crypto/rand\"\n)\n\n// idFunc is a global function that generates aggregate id's.\n// It could be changed from the outside via the SetIDFunc function.\nvar idFunc = randSeq\n\n// SetIDFunc is used to change how aggregate ID's are generated\n// default is a random string\nfunc SetIDFunc(f func() string) {\n\tidFunc = f\n}\n\nfunc randSeq() string {\n\tid, err := generateRandomString(20)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\treturn id\n}\n\nfunc generateRandomString(n int) (string, error) {\n\tconst letters = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-\"\n\tbytes, err := generateRandomBytes(n)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfor i, b := range bytes {\n\t\tbytes[i] = letters[b%byte(len(letters))]\n\t}\n\treturn string(bytes), nil\n}\n\nfunc generateRandomBytes(n int) ([]byte, error) {\n\tb := make([]byte, n)\n\t_, err := rand.Read(b)\n\treturn b, err\n}\n"
  },
  {
    "path": "aggregate/root.go",
    "content": "package aggregate\n\nimport (\n\t\"reflect\"\n\t\"time\"\n\n\t\"github.com/hallgren/eventsourcing\"\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\n// Root to be included into aggregates to give it the aggregate root behaviors\ntype Root struct {\n\tid            string\n\tversion       eventsourcing.Version\n\tglobalVersion eventsourcing.Version\n\tevents        []eventsourcing.Event\n}\n\nconst emptyID = \"\"\n\n// TrackChange is used internally by behaviour methods to apply a state change to\n// the current instance and also track it in order that it can be persisted later.\nfunc TrackChange(a aggregate, data interface{}) {\n\tTrackChangeWithMetadata(a, data, nil)\n}\n\n// TrackChangeWithMetadata is used internally by behaviour methods to apply a state change to\n// the current instance and also track it in order that it can be persisted later.\n// meta data is handled by this func to store none related application state\nfunc TrackChangeWithMetadata(a aggregate, data interface{}, metadata map[string]interface{}) {\n\tar := a.root()\n\t// This can be overwritten in the constructor of the aggregate\n\tif ar.id == emptyID {\n\t\tar.id = idFunc()\n\t}\n\n\tevent := eventsourcing.NewEvent(\n\t\tcore.Event{\n\t\t\tAggregateID:   ar.id,\n\t\t\tVersion:       ar.nextVersion(),\n\t\t\tAggregateType: aggregateType(a),\n\t\t\tTimestamp:     time.Now().UTC(),\n\t\t},\n\t\tdata,\n\t\tmetadata,\n\t)\n\tar.events = append(ar.events, event)\n\ta.Transition(event)\n}\n\n// buildFromHistory builds the aggregate state from events\nfunc buildFromHistory(a aggregate, events []eventsourcing.Event) {\n\troot := a.root()\n\tfor _, event := range events {\n\t\ta.Transition(event)\n\t\t//Set the aggregate ID\n\t\troot.id = event.AggregateID()\n\t\t// Make sure the aggregate is in the correct version (the last event)\n\t\troot.version = event.Version()\n\t\troot.globalVersion = event.GlobalVersion()\n\t}\n}\n\nfunc (ar *Root) nextVersion() core.Version {\n\treturn core.Version(ar.Version()) + 1\n}\n\n// SetID opens up the possibility to set manual aggregate ID from the outside\nfunc (ar *Root) SetID(id string) error {\n\tif ar.id != emptyID {\n\t\treturn eventsourcing.ErrAggregateAlreadyExists\n\t}\n\tar.id = id\n\treturn nil\n}\n\n// ID returns the aggregate ID as a string\nfunc (ar *Root) ID() string {\n\treturn ar.id\n}\n\n// root returns the included Aggregate Root state, and is used from the interface Aggregate.\n//\n//nolint:unused\nfunc (ar *Root) root() *Root {\n\treturn ar\n}\n\n// Version return the version based on events that are not stored\nfunc (ar *Root) Version() eventsourcing.Version {\n\tif len(ar.events) > 0 {\n\t\treturn ar.events[len(ar.events)-1].Version()\n\t}\n\treturn eventsourcing.Version(ar.version)\n}\n\n// GlobalVersion returns the global version based on the last stored event\nfunc (ar *Root) GlobalVersion() eventsourcing.Version {\n\treturn eventsourcing.Version(ar.globalVersion)\n}\n\n// Events return the aggregate events from the aggregate\n// make a copy of the slice preventing outsiders modifying events.\n//\n//nolint:gosimple // for some reason copy does not work\nfunc (ar *Root) Events() []eventsourcing.Event {\n\te := make([]eventsourcing.Event, len(ar.events))\n\t// convert internal event to external event\n\tfor i, event := range ar.events {\n\t\te[i] = event\n\t}\n\treturn e\n}\n\n// UnsavedEvents return true if there's unsaved events on the aggregate\nfunc (ar *Root) UnsavedEvents() bool {\n\treturn len(ar.events) > 0\n}\n\nfunc aggregateType(a interface{}) string {\n\treturn reflect.TypeOf(a).Elem().Name()\n}\n"
  },
  {
    "path": "aggregate/root_test.go",
    "content": "package aggregate_test\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/hallgren/eventsourcing\"\n\t\"github.com/hallgren/eventsourcing/aggregate\"\n)\n\n// Person aggregate\ntype Person struct {\n\taggregate.Root\n\tName string\n\tAge  int\n\tDead int\n}\n\n// Born event\ntype Born struct {\n\tName string\n}\n\n// AgedOneYear event\ntype AgedOneYear struct {\n}\n\n// CreatePerson constructor for the Person\nfunc CreatePerson(name string) (*Person, error) {\n\tif name == \"\" {\n\t\treturn nil, errors.New(\"name can't be blank\")\n\t}\n\tperson := Person{}\n\taggregate.TrackChange(&person, &Born{Name: name})\n\treturn &person, nil\n}\n\n// CreatePersonWithID constructor for the Person that sets the aggregate ID from the outside\nfunc CreatePersonWithID(id, name string) (*Person, error) {\n\tif name == \"\" {\n\t\treturn nil, errors.New(\"name can't be blank\")\n\t}\n\n\tperson := Person{}\n\terr := person.SetID(id)\n\tif err == eventsourcing.ErrAggregateAlreadyExists {\n\t\treturn nil, err\n\t} else if err != nil {\n\t\treturn nil, err\n\t}\n\taggregate.TrackChange(&person, &Born{Name: name})\n\treturn &person, nil\n}\n\n// GrowOlder command\nfunc (person *Person) GrowOlder() {\n\tmetaData := make(map[string]interface{})\n\tmetaData[\"foo\"] = \"bar\"\n\taggregate.TrackChangeWithMetadata(person, &AgedOneYear{}, metaData)\n}\n\n// Register bind the events to the repository when the aggregate is registered.\nfunc (person *Person) Register(f aggregate.RegisterFunc) {\n\tf(&Born{}, &AgedOneYear{})\n}\n\n// Transition the person state dependent on the events\nfunc (person *Person) Transition(event eventsourcing.Event) {\n\tswitch e := event.Data().(type) {\n\tcase *Born:\n\t\tperson.Age = 0\n\t\tperson.Name = e.Name\n\tcase *AgedOneYear:\n\t\tperson.Age += 1\n\t}\n}\n\nfunc (person *Person) SerializeSnapshot(aggregate.SnapshotMarshal) ([]byte, error) {\n\treturn json.Marshal(person)\n}\nfunc (person *Person) DeserializeSnapshot(f aggregate.SnapshotUnmarshal, d []byte) error {\n\treturn json.Unmarshal(d, person)\n}\n\nfunc TestPersonWithNoEvents(t *testing.T) {\n\tperson := Person{}\n\tif person.Version() != 0 {\n\t\tt.Fatalf(\"should have version 0 had %d\", person.Version())\n\t}\n}\n\nfunc TestCreateNewPerson(t *testing.T) {\n\ttimeBefore := time.Now().UTC()\n\tperson, err := CreatePerson(\"kalle\")\n\tif err != nil {\n\t\tt.Fatal(\"Error when creating person\", err.Error())\n\t}\n\n\tif person.Name != \"kalle\" {\n\t\tt.Fatal(\"Wrong person Name\")\n\t}\n\n\tif person.Age != 0 {\n\t\tt.Fatal(\"Wrong person Age\")\n\t}\n\n\tif len(person.Events()) != 1 {\n\t\tt.Fatal(\"There should be one event on the person aggregateRoot\")\n\t}\n\n\tif person.Version() != 1 {\n\t\tt.Fatal(\"Wrong version on the person aggregateRoot\", person.Version())\n\t}\n\n\tif person.Events()[0].Timestamp().Before(timeBefore) {\n\t\tt.Fatal(\"event timestamp before timeBefore\")\n\t}\n\n\tif person.Events()[0].Timestamp().After(time.Now().UTC()) {\n\t\tt.Fatal(\"event timestamp after current time\")\n\t}\n\n\tif person.Events()[0].GlobalVersion() != 0 {\n\t\tt.Fatalf(\"global version should not be set when event is created, was %d\", person.Events()[0].GlobalVersion())\n\t}\n\n\tif !person.UnsavedEvents() {\n\t\tt.Fatal(\"there should be event on the aggregate\")\n\t}\n}\n\nfunc TestCreateNewPersonWithIDFromOutside(t *testing.T) {\n\tid := \"123\"\n\tperson, err := CreatePersonWithID(id, \"kalle\")\n\tif err != nil {\n\t\tt.Fatal(\"Error when creating person\", err.Error())\n\t}\n\n\tif person.ID() != id {\n\t\tt.Fatal(\"Wrong aggregate ID on the person aggregateRoot\", person.ID())\n\t}\n}\n\nfunc TestBlankName(t *testing.T) {\n\t_, err := CreatePerson(\"\")\n\tif err == nil {\n\t\tt.Fatal(\"The constructor should return error on blank Name\")\n\t}\n}\n\nfunc TestSetIDOnExistingPerson(t *testing.T) {\n\tperson, err := CreatePerson(\"Kalle\")\n\tif err != nil {\n\t\tt.Fatal(\"The constructor returned error\")\n\t}\n\n\terr = person.SetID(\"new_id\")\n\tif err == nil {\n\t\tt.Fatal(\"Should not be possible to set ID on already existing person\")\n\t}\n}\n\nfunc TestPersonAgedOneYear(t *testing.T) {\n\tperson, _ := CreatePerson(\"kalle\")\n\tperson.GrowOlder()\n\n\tif len(person.Events()) != 2 {\n\t\tt.Fatal(\"There should be two event on the person aggregateRoot\", person.Events())\n\t}\n\n\tif person.Events()[len(person.Events())-1].Reason() != \"AgedOneYear\" {\n\t\tt.Fatal(\"The last event reason should be AgedOneYear\", person.Events()[len(person.Events())-1].Reason())\n\t}\n\n\td, ok := person.Events()[1].Metadata()[\"foo\"]\n\n\tif !ok {\n\t\tt.Fatal(\"meta data not present\")\n\t}\n\n\tif d.(string) != \"bar\" {\n\t\tt.Fatal(\"wrong meta data\")\n\t}\n\n\tif person.ID() == \"\" {\n\t\tt.Fatal(\"aggregate ID should not be empty\")\n\t}\n}\n\nfunc TestPersonGrewTenYears(t *testing.T) {\n\tperson, _ := CreatePerson(\"kalle\")\n\tfor i := 1; i <= 10; i++ {\n\t\tperson.GrowOlder()\n\t}\n\n\tif person.Age != 10 {\n\t\tt.Fatal(\"person has the wrong Age\")\n\t}\n}\n\nfunc TestSetIDFunc(t *testing.T) {\n\tvar counter = 0\n\tf := func() string {\n\t\tcounter++\n\t\treturn fmt.Sprint(counter)\n\t}\n\n\taggregate.SetIDFunc(f)\n\tfor i := 1; i < 10; i++ {\n\t\tperson, _ := CreatePerson(\"kalle\")\n\t\tif person.ID() != fmt.Sprint(i) {\n\t\t\tt.Fatalf(\"id not set via the new SetIDFunc, exp: %d got: %s\", i, person.ID())\n\t\t}\n\t}\n}\n\nfunc TestIDFuncGeneratingRandomIDs(t *testing.T) {\n\tvar ids = map[string]struct{}{}\n\tfor i := 1; i < 100000; i++ {\n\t\tperson, _ := CreatePerson(\"kalle\")\n\t\t_, exists := ids[person.ID()]\n\t\tif exists {\n\t\t\tt.Fatalf(\"id: %s, already created\", person.ID())\n\t\t}\n\t\tids[person.ID()] = struct{}{}\n\t}\n}\n"
  },
  {
    "path": "aggregate/snapshot.go",
    "content": "package aggregate\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"reflect\"\n\n\t\"github.com/hallgren/eventsourcing\"\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/internal\"\n)\n\ntype SnapshotMarshal func(v interface{}) ([]byte, error)\ntype SnapshotUnmarshal func(data []byte, v interface{}) error\n\n// snapshot interface is used to serialize an aggregate that has properties that are not exported\ntype snapshot interface {\n\troot() *Root\n\tSerializeSnapshot(f SnapshotMarshal) ([]byte, error)\n\tDeserializeSnapshot(f SnapshotUnmarshal, d []byte) error\n}\n\ntype aggregateSnapshot interface {\n\taggregate\n\tsnapshot\n}\n\n// LoadSnapshot build the aggregate based on its snapshot data not including its events.\n// Beware that it could be more events that has happened after the snapshot was taken\nfunc LoadSnapshot(ctx context.Context, ss core.SnapshotStore, id string, s snapshot) error {\n\tif reflect.ValueOf(s).Kind() != reflect.Ptr {\n\t\treturn eventsourcing.ErrAggregateNeedsToBeAPointer\n\t}\n\terr := getSnapshot(ctx, ss, id, s)\n\tif err != nil && errors.Is(err, core.ErrSnapshotNotFound) {\n\t\treturn eventsourcing.ErrAggregateNotFound\n\t}\n\treturn err\n}\n\nfunc getSnapshot(ctx context.Context, ss core.SnapshotStore, id string, s snapshot) error {\n\tsnap, err := ss.Get(ctx, id, aggregateType(s))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = s.DeserializeSnapshot(internal.SnapshotEncoder.Deserialize, snap.State)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// set the internal aggregate properties\n\troot := s.root()\n\troot.globalVersion = eventsourcing.Version(snap.GlobalVersion)\n\troot.version = eventsourcing.Version(snap.Version)\n\troot.id = snap.ID\n\n\treturn nil\n}\n\n// SaveSnapshot will only store the snapshot and will return an error if there are events that are not stored\nfunc SaveSnapshot(ss core.SnapshotStore, s snapshot) error {\n\troot := s.root()\n\tif len(root.Events()) > 0 {\n\t\treturn eventsourcing.ErrUnsavedEvents\n\t}\n\n\tstate := []byte{}\n\tvar err error\n\tstate, err = s.SerializeSnapshot(internal.SnapshotEncoder.Serialize)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsnapshot := core.Snapshot{\n\t\tID:            root.ID(),\n\t\tType:          aggregateType(s),\n\t\tVersion:       core.Version(root.Version()),\n\t\tGlobalVersion: core.Version(root.GlobalVersion()),\n\t\tState:         state,\n\t}\n\n\treturn ss.Save(snapshot)\n}\n"
  },
  {
    "path": "aggregate/snapshot_test.go",
    "content": "package aggregate_test\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing\"\n\t\"github.com/hallgren/eventsourcing/aggregate\"\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/eventstore/memory\"\n\tsnap \"github.com/hallgren/eventsourcing/snapshotstore/memory\"\n)\n\nfunc createPerson() *Person {\n\tes := memory.Create()\n\taggregate.Register(&Person{})\n\tperson, err := CreatePerson(\"kalle\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\taggregate.Save(es, person)\n\n\treturn person\n}\n\nfunc TestSaveAndGetSnapshot(t *testing.T) {\n\tsnapshotStore := snap.Create()\n\tperson := createPerson()\n\terr := aggregate.SaveSnapshot(snapshotStore, person)\n\tif err != nil {\n\t\tt.Fatalf(\"could not save aggregate, err: %v\", err)\n\t}\n\n\ttwin := Person{}\n\terr = aggregate.LoadSnapshot(context.Background(), snapshotStore, person.ID(), &twin)\n\tif err != nil {\n\t\tt.Fatalf(\"could not get aggregate, err: %v\", err)\n\t}\n\n\t// Check internal aggregate version\n\tif person.Version() != twin.Version() {\n\t\tt.Fatalf(\"Wrong version org %q copy %q\", person.Version(), twin.Version())\n\t}\n\n\tif person.ID() != twin.ID() {\n\t\tt.Fatalf(\"Wrong id org %q copy %q\", person.ID(), twin.ID())\n\t}\n\n\tif person.Name != twin.Name {\n\t\tt.Fatalf(\"Wrong name org: %q copy %q\", person.Name, twin.Name)\n\t}\n}\n\nfunc TestGetNoneExistingSnapshotOrEvents(t *testing.T) {\n\tsnapshotStore := snap.Create()\n\tperson := Person{}\n\n\terr := aggregate.LoadSnapshot(context.Background(), snapshotStore, \"none_existing_id\", &person)\n\tif !errors.Is(err, eventsourcing.ErrAggregateNotFound) {\n\t\tt.Fatal(\"should get error when no snapshot or event stored for aggregate\")\n\t}\n}\n\nfunc TestGetNoneExistingSnapshot(t *testing.T) {\n\tsnapshotStore := snap.Create()\n\n\tperson := Person{}\n\terr := aggregate.LoadSnapshot(context.Background(), snapshotStore, \"none_existing_id\", &person)\n\tif !errors.Is(err, eventsourcing.ErrAggregateNotFound) {\n\t\tt.Fatal(\"should get error when no snapshot stored for aggregate\")\n\t}\n}\n\nfunc TestSaveSnapshotWithUnsavedEvents(t *testing.T) {\n\tsnapshotStore := snap.Create()\n\n\tperson, err := CreatePerson(\"kalle\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = aggregate.SaveSnapshot(snapshotStore, person)\n\tif err == nil {\n\t\tt.Fatalf(\"should not be able to save snapshot with unsaved events\")\n\t}\n}\n\n// test custom snapshot struct to handle non-exported properties on aggregate\ntype snapshot struct {\n\taggregate.Root\n\tunexported string\n\tExported   string\n\t// to be able to save the snapshot after events are added to it.\n\trepo core.EventStore\n}\n\ntype Event struct{}\ntype Event2 struct{}\n\nfunc New() *snapshot {\n\tes := memory.Create()\n\taggregate.Register(&snapshot{})\n\ts := snapshot{}\n\taggregate.TrackChange(&s, &Event{})\n\ts.repo = es\n\taggregate.Save(es, &s)\n\treturn &s\n}\n\nfunc (s *snapshot) Command() {\n\taggregate.TrackChange(s, &Event2{})\n\taggregate.Save(s.repo, s)\n}\n\nfunc (s *snapshot) Transition(e eventsourcing.Event) {\n\tswitch e.Data().(type) {\n\tcase *Event:\n\t\ts.unexported = \"unexported\"\n\t\ts.Exported = \"Exported\"\n\tcase *Event2:\n\t\ts.unexported = \"unexported2\"\n\t\ts.Exported = \"Exported2\"\n\t}\n}\n\n// Register bind the events to the repository when the aggregate is registered.\nfunc (s *snapshot) Register(f aggregate.RegisterFunc) {\n\tf(&Event{}, &Event2{})\n}\n\ntype snapshotInternal struct {\n\tUnExported string\n\tExported   string\n}\n\nfunc (s *snapshot) SerializeSnapshot(f aggregate.SnapshotMarshal) ([]byte, error) {\n\tsnap := snapshotInternal{\n\t\tUnExported: s.unexported,\n\t\tExported:   s.Exported,\n\t}\n\treturn f(snap)\n}\n\nfunc (s *snapshot) DeserializeSnapshot(f aggregate.SnapshotUnmarshal, b []byte) error {\n\tsnap := snapshotInternal{}\n\terr := f(b, &snap)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.unexported = snap.UnExported\n\ts.Exported = snap.Exported\n\treturn nil\n}\n\nfunc TestSnapshotNoneExported(t *testing.T) {\n\tsnapshotStore := snap.Create()\n\n\tsnap := New()\n\terr := aggregate.SaveSnapshot(snapshotStore, snap)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tsnap.Command()\n\terr = aggregate.SaveSnapshot(snapshotStore, snap)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tsnap2 := snapshot{}\n\terr = aggregate.LoadSnapshot(context.Background(), snapshotStore, snap.ID(), &snap2)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif snap.unexported != snap2.unexported {\n\t\tt.Fatalf(\"none exported value differed %s %s\", snap.unexported, snap2.unexported)\n\t}\n\n\tif snap.Exported != snap2.Exported {\n\t\tt.Fatalf(\"exported value differed %s %s\", snap.Exported, snap2.Exported)\n\t}\n}\n"
  },
  {
    "path": "core/event.go",
    "content": "package core\n\nimport (\n\t\"time\"\n)\n\n// Version is the event version used in event.Version and event.GlobalVersio\ntype Version uint64\n\n// Event holding meta data and the application specific event in the Data property\ntype Event struct {\n\tAggregateID   string\n\tVersion       Version\n\tGlobalVersion Version\n\tAggregateType string\n\tTimestamp     time.Time\n\tReason        string // based on the Data type\n\tData          []byte // interface{} on the external Event type\n\tMetadata      []byte // map[string]interface{} on the external Event type\n}\n"
  },
  {
    "path": "core/eventstore.go",
    "content": "package core\n\nimport (\n\t\"context\"\n\t\"errors\"\n)\n\n// ErrConcurrency when the currently saved version of the aggregate differs from the new ones\nvar ErrConcurrency = errors.New(\"concurrency error\")\n\n// Iterator is the interface an event store Get needs to return\ntype Iterator interface {\n\tNext() bool\n\tValue() (Event, error)\n\tClose()\n}\n\n// EventStore interface expose the methods an event store must uphold\ntype EventStore interface {\n\tSave(events []Event) error\n\tGet(ctx context.Context, id string, aggregateType string, afterVersion Version) (Iterator, error)\n}\n"
  },
  {
    "path": "core/fetcher.go",
    "content": "package core\n\n// Fetcher is the event fetch function concumed by projections\ntype Fetcher func() (Iterator, error)\n"
  },
  {
    "path": "core/go.mod",
    "content": "module github.com/hallgren/eventsourcing/core\n\ngo 1.13\n"
  },
  {
    "path": "core/snapshotstore.go",
    "content": "package core\n\nimport (\n\t\"context\"\n\t\"errors\"\n)\n\n// ErrSnapshotNotFound returned when no snapshot is found in the snapshot store\nvar ErrSnapshotNotFound = errors.New(\"snapshot not found\")\n\n// Snapshot holds current state of an aggregate\ntype Snapshot struct {\n\tID            string\n\tType          string\n\tVersion       Version\n\tGlobalVersion Version\n\tState         []byte\n}\n\n// SnapshotStore expose the methods a snapshot store must uphold\ntype SnapshotStore interface {\n\tSave(snapshot Snapshot) error\n\tGet(ctx context.Context, id, aggregateType string) (Snapshot, error)\n}\n"
  },
  {
    "path": "core/testsuite/eventstoresuite.go",
    "content": "package testsuite\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\nvar seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))\n\nfunc AggregateID() string {\n\tr := seededRand.Intn(999999999999)\n\treturn fmt.Sprintf(\"%d\", r)\n}\n\ntype eventstoreFunc = func() (core.EventStore, func(), error)\n\n// Status represents the Red, Silver or Gold tier level of a FrequentFlierAccount\ntype Status int\n\nconst (\n\tStatusRed    Status = iota\n\tStatusSilver Status = iota\n\tStatusGold   Status = iota\n)\n\ntype FrequentFlierAccountCreated struct {\n\tAccountId         string\n\tOpeningMiles      int\n\tOpeningTierPoints int\n}\n\ntype StatusMatched struct {\n\tNewStatus Status\n}\n\ntype FlightTaken struct {\n\tMilesAdded      int\n\tTierPointsAdded int\n}\n\nvar aggregateType = \"FrequentFlierAccount\"\nvar timestamp = time.Now()\n\nfunc eventToByte(i interface{}) []byte {\n\tb, _ := json.Marshal(i)\n\treturn b\n}\n\nfunc testEvents(aggregateID string) []core.Event {\n\tmetadata := make(map[string]interface{})\n\tmetadata[\"test\"] = \"hello\"\n\thistory := []core.Event{\n\t\t{AggregateID: aggregateID, Version: 1, AggregateType: aggregateType, Timestamp: timestamp, Reason: \"FrequentFlierAccountCreated\", Data: eventToByte(&FrequentFlierAccountCreated{AccountId: \"1234567\", OpeningMiles: 10000, OpeningTierPoints: 0}), Metadata: eventToByte(metadata)},\n\t\t{AggregateID: aggregateID, Version: 2, AggregateType: aggregateType, Timestamp: timestamp, Reason: \"StatusMatched\", Data: eventToByte(&StatusMatched{NewStatus: StatusSilver}), Metadata: eventToByte(metadata)},\n\t\t{AggregateID: aggregateID, Version: 3, AggregateType: aggregateType, Timestamp: timestamp, Reason: \"FlightTaken\", Data: eventToByte(&FlightTaken{MilesAdded: 2525, TierPointsAdded: 5}), Metadata: eventToByte(metadata)},\n\t\t{AggregateID: aggregateID, Version: 4, AggregateType: aggregateType, Timestamp: timestamp, Reason: \"FlightTaken\", Data: eventToByte(&FlightTaken{MilesAdded: 2512, TierPointsAdded: 5}), Metadata: eventToByte(metadata)},\n\t\t{AggregateID: aggregateID, Version: 5, AggregateType: aggregateType, Timestamp: timestamp, Reason: \"FlightTaken\", Data: eventToByte(&FlightTaken{MilesAdded: 5600, TierPointsAdded: 5}), Metadata: eventToByte(metadata)},\n\t\t{AggregateID: aggregateID, Version: 6, AggregateType: aggregateType, Timestamp: timestamp, Reason: \"FlightTaken\", Data: eventToByte(&FlightTaken{MilesAdded: 3000, TierPointsAdded: 3}), Metadata: eventToByte(metadata)},\n\t}\n\treturn history\n}\n\nfunc testEventsPartTwo(aggregateID string) []core.Event {\n\thistory := []core.Event{\n\t\t{AggregateID: aggregateID, Version: 7, AggregateType: aggregateType, Timestamp: timestamp, Reason: \"FlightTaken\", Data: eventToByte(&FlightTaken{MilesAdded: 5600, TierPointsAdded: 5})},\n\t\t{AggregateID: aggregateID, Version: 8, AggregateType: aggregateType, Timestamp: timestamp, Reason: \"FlightTaken\", Data: eventToByte(&FlightTaken{MilesAdded: 3000, TierPointsAdded: 3})},\n\t}\n\treturn history\n}\n\nfunc testEventOtherAggregate(aggregateID string) core.Event {\n\treturn core.Event{AggregateID: aggregateID, Version: 1, AggregateType: aggregateType, Timestamp: timestamp, Reason: \"FrequentFlierAccountCreated\", Data: eventToByte(&FrequentFlierAccountCreated{AccountId: \"1234567\", OpeningMiles: 10000, OpeningTierPoints: 0})}\n}\n\nfunc Test(t *testing.T, esFunc eventstoreFunc) {\n\ttests := []struct {\n\t\ttitle string\n\t\trun   func(es core.EventStore) error\n\t}{\n\t\t{\"should save and get events\", saveAndGetEvents},\n\t\t{\"should get events after version\", getEventsAfterVersion},\n\t\t{\"should not save events in wrong version\", saveEventsInWrongVersion},\n\t\t{\"should save and get event concurrently\", saveAndGetEventsConcurrently},\n\t\t{\"should return error when no events\", getErrWhenNoEvents},\n\t\t{\"should get global event order from save\", saveReturnGlobalEventOrder},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.title, func(t *testing.T) {\n\t\t\tes, closeFunc, err := esFunc()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\terr = test.run(es)\n\t\t\tif err != nil {\n\t\t\t\t// make use of t.Error instead of t.Fatal to make sure the closeFunc is executed\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\tcloseFunc()\n\t\t})\n\t}\n}\n\nfunc saveAndGetEvents(es core.EventStore) error {\n\taggregateID := AggregateID()\n\tevents := testEvents(aggregateID)\n\tfetchedEvents := []core.Event{}\n\terr := es.Save(events)\n\tif err != nil {\n\t\treturn err\n\t}\n\titerator, err := es.Get(context.Background(), aggregateID, aggregateType, 0)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor iterator.Next() {\n\t\tevent, err := iterator.Value()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfetchedEvents = append(fetchedEvents, event)\n\t}\n\titerator.Close()\n\tif len(fetchedEvents) != len(testEvents(aggregateID)) {\n\t\treturn errors.New(\"wrong number of events returned\")\n\t}\n\n\tif fetchedEvents[0].Version != testEvents(aggregateID)[0].Version {\n\t\treturn errors.New(\"wrong events returned\")\n\t}\n\n\t// Add more events to the same aggregate event stream\n\terr = es.Save(testEventsPartTwo(aggregateID))\n\tif err != nil {\n\t\treturn err\n\t}\n\tfetchedEventsIncludingPartTwo := []core.Event{}\n\titerator, err = es.Get(context.Background(), aggregateID, aggregateType, 0)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor iterator.Next() {\n\t\tevent, err := iterator.Value()\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tfetchedEventsIncludingPartTwo = append(fetchedEventsIncludingPartTwo, event)\n\t}\n\titerator.Close()\n\n\tif len(fetchedEventsIncludingPartTwo) != len(append(testEvents(aggregateID), testEventsPartTwo(aggregateID)...)) {\n\t\treturn errors.New(\"wrong number of events returned\")\n\t}\n\n\tif fetchedEventsIncludingPartTwo[0].Version != testEvents(aggregateID)[0].Version {\n\t\treturn errors.New(\"wrong event version returned\")\n\t}\n\n\tif fetchedEventsIncludingPartTwo[0].AggregateID != testEvents(aggregateID)[0].AggregateID {\n\t\treturn errors.New(\"wrong event aggregateID returned\")\n\t}\n\n\tif fetchedEventsIncludingPartTwo[0].AggregateType != testEvents(aggregateID)[0].AggregateType {\n\t\treturn errors.New(\"wrong event aggregateType returned\")\n\t}\n\n\tif fetchedEventsIncludingPartTwo[0].Reason != testEvents(aggregateID)[0].Reason {\n\t\treturn errors.New(\"wrong event reason returned\")\n\t}\n\treturn nil\n}\n\nfunc getEventsAfterVersion(es core.EventStore) error {\n\tvar fetchedEvents []core.Event\n\taggregateID := AggregateID()\n\terr := es.Save(testEvents(aggregateID))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\titerator, err := es.Get(context.Background(), aggregateID, aggregateType, 1)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor iterator.Next() {\n\t\tevent, err := iterator.Value()\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tfetchedEvents = append(fetchedEvents, event)\n\t}\n\titerator.Close()\n\t// Should return one less event\n\tif len(fetchedEvents) != len(testEvents(aggregateID))-1 {\n\t\treturn fmt.Errorf(\"wrong number of events returned exp: %d, got:%d\", len(fetchedEvents), len(testEvents(aggregateID))-1)\n\t}\n\t// first event version should be 2\n\tif fetchedEvents[0].Version != 2 {\n\t\treturn fmt.Errorf(\"wrong events returned\")\n\t}\n\treturn nil\n}\n\nfunc saveEventsInWrongVersion(es core.EventStore) error {\n\taggregateID := AggregateID()\n\tevents := testEventsPartTwo(aggregateID)\n\terr := es.Save(events)\n\n\tif !errors.Is(err, core.ErrConcurrency) {\n\t\treturn errors.New(\"should not be able to save events that are out of sync compared to the storage order\")\n\t}\n\treturn nil\n}\n\nfunc saveAndGetEventsConcurrently(es core.EventStore) error {\n\twg := sync.WaitGroup{}\n\tvar err error\n\taggregateID := AggregateID()\n\n\twg.Add(10)\n\tfor i := 0; i < 10; i++ {\n\t\tevents := testEvents(fmt.Sprintf(\"%s-%d\", aggregateID, i))\n\t\tgo func() {\n\t\t\te := es.Save(events)\n\t\t\tif e != nil {\n\t\t\t\terr = e\n\t\t\t}\n\t\t\twg.Done()\n\t\t}()\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\twg.Wait()\n\twg.Add(10)\n\tfor i := 0; i < 10; i++ {\n\t\teventID := fmt.Sprintf(\"%s-%d\", aggregateID, i)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\titerator, e := es.Get(context.Background(), eventID, aggregateType, 0)\n\t\t\tif e != nil {\n\t\t\t\terr = e\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevents := make([]core.Event, 0)\n\t\t\tfor iterator.Next() {\n\t\t\t\tevent, err := iterator.Value()\n\t\t\t\tif err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\tevents = append(events, event)\n\t\t\t}\n\t\t\titerator.Close()\n\t\t\tif len(events) != 6 {\n\t\t\t\terr = fmt.Errorf(\"wrong number of events fetched, expecting 6 got %d\", len(events))\n\t\t\t\treturn\n\t\t\t}\n\t\t}()\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\twg.Wait()\n\treturn nil\n}\n\nfunc getErrWhenNoEvents(es core.EventStore) error {\n\taggregateID := AggregateID()\n\titerator, err := es.Get(context.Background(), aggregateID, aggregateType, 0)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer iterator.Close()\n\tif iterator.Next() {\n\t\treturn fmt.Errorf(\"expect no event when no events are saved\")\n\t}\n\treturn nil\n}\n\nfunc saveReturnGlobalEventOrder(es core.EventStore) error {\n\taggregateID := AggregateID()\n\taggregateID2 := AggregateID()\n\tevents := testEvents(aggregateID)\n\terr := es.Save(events)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif events[len(events)-1].GlobalVersion == 0 {\n\t\treturn fmt.Errorf(\"expected global event order > 0 on last event got %d\", events[len(events)-1].GlobalVersion)\n\t}\n\tevents2 := []core.Event{testEventOtherAggregate(aggregateID2)}\n\terr = es.Save(events2)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif events2[0].GlobalVersion <= events[len(events)-1].GlobalVersion {\n\t\treturn fmt.Errorf(\"expected larger global event order got %d\", events2[0].GlobalVersion)\n\t}\n\treturn nil\n}\n\n/* re-activate when esdb eventstore have global event order on each stream\nfunc setGlobalVersionOnSavedEvents(es eventsourcing.EventStore) error {\n\tevents := testEvents()\n\terr := es.Save(events)\n\tif err != nil {\n\t\treturn err\n\t}\n\teventsGet, err := es.Get(events[0].AggregateID, events[0].AggregateType, 0)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar g eventsourcing.Version\n\tfor _, e := range eventsGet {\n\t\tg++\n\t\tif e.GlobalVersion != g {\n\t\t\treturn fmt.Errorf(\"expected global version to be in sequens exp: %d, was: %d\", g, e.GlobalVersion)\n\t\t}\n\t}\n\treturn nil\n}\n*/\n"
  },
  {
    "path": "core/testsuite/fetcher.go",
    "content": "package testsuite\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\nfunc TestFetcher(t *testing.T, es core.EventStore, fetcher core.Fetcher) {\n\tglobalVersion := core.Version(0)\n\taggregateID := AggregateID()\n\n\tevents := testEvents(aggregateID)\n\terr := es.Save(events)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\titer, err := fetcher()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = verify(iter, globalVersion, events)\n\tif err != nil {\n\t\titer.Close()\n\t\tt.Fatal(err)\n\t}\n\titer.Close()\n\n\t// set the globalVersion to the length of the events as not all event stores update the\n\t// globalVersion on the events after they are saved\n\tglobalVersion = core.Version(len(events))\n\n\tevents2 := testEventsPartTwo(aggregateID)\n\terr = es.Save(events2)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// run fetcher second time - should not restart from first event\n\titer2, err := fetcher()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer iter2.Close()\n\terr = verify(iter2, globalVersion, events2)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// verifies that the events from the iterator has a globalversion higher than sent in globalVersion and that\n// the events are the same from the iterator and sent in event.\nfunc verify(iter core.Iterator, globalVersion core.Version, expEvents []core.Event) error {\n\ti := 0\n\tfor iter.Next() {\n\t\tevent, err := iter.Value()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif event.Version != expEvents[i].Version && event.AggregateID != expEvents[i].AggregateID {\n\t\t\treturn fmt.Errorf(\"missmatch in expected event version %q and actual version %q, expected aggregate id %q actual aggregate id %q\", expEvents[i].Version, event.Version, expEvents[i].AggregateID, event.AggregateID)\n\t\t}\n\t\tif event.GlobalVersion <= globalVersion {\n\t\t\treturn fmt.Errorf(\"event global version (%q) is lower than previos event %q\", event.GlobalVersion, globalVersion)\n\t\t}\n\t\tglobalVersion = event.GlobalVersion\n\t\ti++\n\t}\n\tif i != len(expEvents) {\n\t\treturn fmt.Errorf(\"expected %d events got %d\", len(expEvents), i)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "core/testsuite/snapshotstoresuite.go",
    "content": "package testsuite\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\ntype snapshotstoreFunc = func() (core.SnapshotStore, func(), error)\n\nfunc TestSnapshotStore(t *testing.T, ssFunc snapshotstoreFunc) {\n\ttests := []struct {\n\t\ttitle string\n\t\trun   func(es core.SnapshotStore) error\n\t}{\n\t\t{\"should save and get snapshot\", saveAndGetSnapshot},\n\t\t{\"should get error when getting none existing snapshot\", getNoneExistingSnapshot},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.title, func(t *testing.T) {\n\t\t\tss, closeFunc, err := ssFunc()\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\terr = test.run(ss)\n\t\t\tif err != nil {\n\t\t\t\t// make use of t.Error instead of t.Fatal to make sure the closeFunc is executed\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t\tcloseFunc()\n\t\t})\n\t}\n}\n\nfunc saveAndGetSnapshot(ss core.SnapshotStore) error {\n\tsnapshot := core.Snapshot{\n\t\tID:            \"id\",\n\t\tType:          \"person\",\n\t\tVersion:       1,\n\t\tGlobalVersion: 1,\n\t\tState:         []byte(\"123\"),\n\t}\n\n\terr := ss.Save(snapshot)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ts, err := ss.Get(context.Background(), \"id\", \"person\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif string(s.State) != \"123\" {\n\t\treturn errors.New(\"wrong snapshot state\")\n\t}\n\n\tif s.Version != snapshot.Version {\n\t\treturn fmt.Errorf(\"exp version %d got %d\", snapshot.Version, s.Version)\n\t}\n\n\tif s.GlobalVersion != snapshot.GlobalVersion {\n\t\treturn fmt.Errorf(\"exp global version %d got %d\", snapshot.GlobalVersion, s.GlobalVersion)\n\t}\n\n\ts, err = ss.Get(context.Background(), \"none_existing_id\", \"person\")\n\tif !errors.Is(err, core.ErrSnapshotNotFound) {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc getNoneExistingSnapshot(ss core.SnapshotStore) error {\n\t_, err := ss.Get(context.Background(), \"id\", \"person\")\n\tif !errors.Is(err, core.ErrSnapshotNotFound) {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "event.go",
    "content": "package eventsourcing\n\nimport (\n\t\"reflect\"\n\t\"time\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\n// Version is the event version used in event.Version and event.GlobalVersion\ntype Version core.Version\n\ntype Event struct {\n\tevent    core.Event // internal event\n\tdata     interface{}\n\tmetadata map[string]interface{}\n}\n\nfunc NewEvent(e core.Event, data interface{}, metadata map[string]interface{}) Event {\n\treturn Event{event: e, data: data, metadata: metadata}\n}\n\nfunc (e Event) Data() interface{} {\n\treturn e.data\n}\n\nfunc (e Event) Metadata() map[string]interface{} {\n\treturn e.metadata\n}\n\nfunc (e Event) AggregateType() string {\n\treturn e.event.AggregateType\n}\n\nfunc (e Event) AggregateID() string {\n\treturn e.event.AggregateID\n}\n\nfunc (e Event) Reason() string {\n\tif e.data == nil {\n\t\treturn \"\"\n\t}\n\treturn reflect.TypeOf(e.data).Elem().Name()\n}\n\nfunc (e Event) Version() Version {\n\treturn Version(e.event.Version)\n}\n\nfunc (e Event) Timestamp() time.Time {\n\treturn e.event.Timestamp\n}\n\nfunc (e Event) GlobalVersion() Version {\n\treturn Version(e.event.GlobalVersion)\n}\n"
  },
  {
    "path": "eventsourcing.go",
    "content": "package eventsourcing\n\nimport (\n\t\"errors\"\n\n\t\"github.com/hallgren/eventsourcing/internal\"\n)\n\nvar (\n\t// ErrAggregateNotFound returns if events not found for aggregate or aggregate was not based on snapshot from the outside\n\tErrAggregateNotFound = errors.New(\"aggregate not found\")\n\n\t// ErrAggregateNotRegistered when saving aggregate when it's not registered in the repository\n\tErrAggregateNotRegistered = errors.New(\"aggregate not registered\")\n\n\t// ErrEventNotRegistered when saving aggregate and one event is not registered in the repository\n\tErrEventNotRegistered = errors.New(\"event not registered\")\n\n\t// ErrConcurrency when the currently saved version of the aggregate differs from the new events\n\tErrConcurrency = errors.New(\"concurrency error\")\n\n\t// ErrAggregateAlreadyExists returned if the aggregateID is set more than one time\n\tErrAggregateAlreadyExists = errors.New(\"its not possible to set ID on already existing aggregate\")\n\n\t// ErrAggregateNeedsToBeAPointer return if aggregate is sent in as value object\n\tErrAggregateNeedsToBeAPointer = errors.New(\"aggregate needs to be a pointer\")\n\n\t// ErrUnsavedEvents aggregate events must be saved before creating snapshot\n\tErrUnsavedEvents = errors.New(\"aggregate holds unsaved events\")\n)\n\n// Encoder is the interface used to Serialize/Deserialize events and snapshots\ntype Encoder interface {\n\tSerialize(v interface{}) ([]byte, error)\n\tDeserialize(data []byte, v interface{}) error\n}\n\n// SetEventEncoder change the default JSON encoder that serialize/deserialize events\nfunc SetEventEncoder(e Encoder) {\n\tinternal.EventEncoder = e\n}\n\n// SetSnapshotEncoder change the default JSON encoder that seialize/deserialize snapshots\nfunc SetSnapshotEncoder(e Encoder) {\n\tinternal.SnapshotEncoder = e\n}\n"
  },
  {
    "path": "eventsourcing_test.go",
    "content": "package eventsourcing_test\n\n/*\nfunc TestGetWithContextCancel(t *testing.T) {\n\tes := memory.Create()\n\taggregate.AggregateRegister(&Person{})\n\n\tperson, err := CreatePerson(\"kalle\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = aggregate.AggregateSave(es, person)\n\tif err != nil {\n\t\tt.Fatal(\"could not save aggregate\")\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\n\t// cancel the context\n\tcancel()\n\terr = eventsourcing.AggregateLoad(ctx, es, person.ID(), person)\n\tif !errors.Is(err, context.Canceled) {\n\t\tt.Fatalf(\"expected error context.Canceled but was %v\", err)\n\t}\n}\n\nfunc TestSaveWhenAggregateNotRegistered(t *testing.T) {\n\tes := memory.Create()\n\teventsourcing.ResetRegsiter()\n\n\tperson, err := CreatePerson(\"kalle\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = eventsourcing.AggregateSave(es, person)\n\tif !errors.Is(err, eventsourcing.ErrAggregateNotRegistered) {\n\t\tt.Fatalf(\"could save aggregate that was not registered, err: %v\", err)\n\t}\n}\n\nfunc TestMultipleSave(t *testing.T) {\n\tes := memory.Create()\n\teventsourcing.AggregateRegister(&Person{})\n\n\tperson, err := CreatePerson(\"kalle\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = eventsourcing.AggregateSave(es, person)\n\tif err != nil {\n\t\tt.Fatalf(\"could not save aggregate, err: %v\", err)\n\t}\n\n\tversion := person.Version()\n\n\terr = eventsourcing.AggregateSave(es, person)\n\tif err != nil {\n\t\tt.Fatalf(\"save should be a nop, err: %v\", err)\n\t}\n\n\tif version != person.Version() {\n\t\tt.Fatalf(\"the nop save should not change the aggregate version exp:%d, actual:%d\", version, person.Version())\n\t}\n}\n\nfunc TestConcurrentRead(t *testing.T) {\n\tes := memory.Create()\n\teventsourcing.AggregateRegister(&Person{})\n\n\tperson, err := CreatePerson(\"kalle\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = eventsourcing.AggregateSave(es, person)\n\tif err != nil {\n\t\tt.Fatal(\"could not save aggregate\")\n\t}\n\tperson2, err := CreatePerson(\"anka\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = eventsourcing.AggregateSave(es, person2)\n\tif err != nil {\n\t\tt.Fatal(\"could not save aggregate\")\n\t}\n\n\tfor i := 1; i <= 1000; i++ {\n\t\tp1 := Person{}\n\t\tp2 := Person{}\n\t\twg := sync.WaitGroup{}\n\t\twg.Add(2)\n\t\tgo func() {\n\t\t\teventsourcing.AggregateLoad(context.Background(), es, person.ID(), &p1)\n\t\t\twg.Done()\n\t\t}()\n\t\tgo func() {\n\t\t\teventsourcing.AggregateLoad(context.Background(), es, person2.ID(), &p2)\n\t\t\twg.Done()\n\t\t}()\n\t\twg.Wait()\n\t\tif p1.Name == p2.Name {\n\t\t\tt.Fatal(\"name should differ\")\n\t\t}\n\t}\n}\n*/\n"
  },
  {
    "path": "eventstore/bbolt/README.md",
    "content": "## BBolt Event Store\n\nThis event store supports the go.etcd.io/bbolt bolt driver.\n\n### Constructor\n\n```go\n// New opens the event stream found in the given file. If the file is not found it will be created and\n// initialized. Will return error if it has problems persisting the changes to the filesystem.\nfunc New(dbFile string) (*BBolt, error) {\n```\n\n### Example of use\n\n```go\nimport \"github.com/hallgren/eventsourcing/eventstore/bbolt\"\n\nbboltEventStore, err := bbolt.New(\"bolt.db\")\nif err != nil {\n\treturn nil, nil, err\n}\n```\n"
  },
  {
    "path": "eventstore/bbolt/bbolt.go",
    "content": "package bbolt\n\nimport (\n\t\"context\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"go.etcd.io/bbolt\"\n)\n\nconst (\n\tglobalEventOrderBucketName = \"global_event_order\"\n)\n\n// BBolt is the eventstore handler\ntype BBolt struct {\n\tdb *bbolt.DB // The bbolt db where we store everything\n}\n\ntype boltEvent struct {\n\tAggregateID   string\n\tVersion       uint64\n\tGlobalVersion uint64\n\tReason        string\n\tAggregateType string\n\tTimestamp     time.Time\n\tData          []byte\n\tMetadata      []byte // map[string]interface{}\n}\n\n// New opens the event stream found in the given file. If the file is not found it will be created and\n// initialized. Will return error if it has problems persisting the changes to the filesystem.\nfunc New(dbFile string) (*BBolt, error) {\n\tdb, err := bbolt.Open(dbFile, 0600, &bbolt.Options{\n\t\tTimeout: 1 * time.Second,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Ensure that we have a bucket to store the global event ordering\n\terr = db.Update(func(tx *bbolt.Tx) error {\n\t\tif _, err := tx.CreateBucketIfNotExists([]byte(globalEventOrderBucketName)); err != nil {\n\t\t\treturn errors.New(\"could not create global event order bucket\")\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &BBolt{\n\t\tdb: db,\n\t}, nil\n}\n\n// Save an aggregate (its events)\nfunc (e *BBolt) Save(events []core.Event) error {\n\t// Return if there is no events to save\n\tif len(events) == 0 {\n\t\treturn nil\n\t}\n\n\t// get bucket name from first event\n\taggregateType := events[0].AggregateType\n\taggregateID := events[0].AggregateID\n\tbucketRef := bucketRef(aggregateType, aggregateID)\n\n\ttx, err := e.db.Begin(true)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer tx.Rollback()\n\n\tevBucket := tx.Bucket(bucketRef)\n\tif evBucket == nil {\n\t\t// Ensure that we have a bucket named events_aggregateType_aggregateID for the given aggregate\n\t\terr = e.createBucket(bucketRef, tx)\n\t\tif err != nil {\n\t\t\treturn errors.New(\"could not create aggregate events bucket\")\n\t\t}\n\t\tevBucket = tx.Bucket(bucketRef)\n\t}\n\n\tcurrentVersion := uint64(0)\n\tcursor := evBucket.Cursor()\n\tk, obj := cursor.Last()\n\tif k != nil {\n\t\tlastEvent := boltEvent{}\n\t\terr := json.Unmarshal(obj, &lastEvent)\n\t\tif err != nil {\n\t\t\treturn errors.New(fmt.Sprintf(\"could not serialize event, %v\", err))\n\t\t}\n\t\tcurrentVersion = lastEvent.Version\n\t}\n\n\t// Make sure no other has saved event to the same aggregate concurrently\n\tif core.Version(currentVersion)+1 != events[0].Version {\n\t\treturn core.ErrConcurrency\n\t}\n\n\tglobalBucket := tx.Bucket([]byte(globalEventOrderBucketName))\n\tif globalBucket == nil {\n\t\treturn errors.New(\"global bucket not found\")\n\t}\n\n\tvar globalSequence uint64\n\tfor i, event := range events {\n\t\tsequence, err := evBucket.NextSequence()\n\t\tif err != nil {\n\t\t\treturn errors.New(fmt.Sprintf(\"could not get sequence for %#v\", string(bucketRef)))\n\t\t}\n\n\t\t// We need to establish a global event order that spans over all buckets. This is so that we can be\n\t\t// able to play the event (or send) them in the order that they was entered into this database.\n\t\t// The global sequence bucket contains an ordered line of pointer to all events on the form bucket_name:seq_num\n\t\tglobalSequence, err = globalBucket.NextSequence()\n\t\tif err != nil {\n\t\t\treturn errors.New(\"could not get next sequence for global bucket\")\n\t\t}\n\n\t\t// build the internal bolt event\n\t\tbEvent := boltEvent{\n\t\t\tAggregateID:   event.AggregateID,\n\t\t\tAggregateType: event.AggregateType,\n\t\t\tVersion:       uint64(event.Version),\n\t\t\tGlobalVersion: globalSequence,\n\t\t\tReason:        event.Reason,\n\t\t\tTimestamp:     event.Timestamp,\n\t\t\tMetadata:      event.Metadata,\n\t\t\tData:          event.Data,\n\t\t}\n\n\t\tvalue, err := json.Marshal(bEvent)\n\t\tif err != nil {\n\t\t\treturn errors.New(fmt.Sprintf(\"could not serialize event, %v\", err))\n\t\t}\n\n\t\terr = evBucket.Put(itob(sequence), value)\n\t\tif err != nil {\n\t\t\treturn errors.New(fmt.Sprintf(\"could not save event %#v in bucket\", event))\n\t\t}\n\t\terr = globalBucket.Put(itob(globalSequence), value)\n\t\tif err != nil {\n\t\t\treturn errors.New(fmt.Sprintf(\"could not save global sequence pointer for %#v\", string(bucketRef)))\n\t\t}\n\n\t\t// override the event in the slice exposing the GlobalVersion to the caller\n\t\tevents[i].GlobalVersion = core.Version(globalSequence)\n\t}\n\treturn tx.Commit()\n}\n\n// Get aggregate events\nfunc (e *BBolt) Get(ctx context.Context, id string, aggregateType string, afterVersion core.Version) (core.Iterator, error) {\n\ttx, err := e.db.Begin(false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbucket := tx.Bucket(bucketRef(aggregateType, id))\n\tif bucket == nil {\n\t\treturn &Iterator{tx: tx}, nil\n\t}\n\tcursor := bucket.Cursor()\n\treturn &Iterator{tx: tx, cursor: cursor, startPosition: position(afterVersion)}, nil\n}\n\n// All iterate over event in GlobalEvents order\nfunc (e *BBolt) All(start core.Version) core.Fetcher {\n\titer := Iterator{}\n\treturn func() (core.Iterator, error) {\n\t\ttx, err := e.db.Begin(false)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tbucket := tx.Bucket([]byte(globalEventOrderBucketName))\n\t\tif bucket == nil {\n\t\t\treturn &Iterator{tx: tx}, nil\n\t\t}\n\t\t// set start from second call and forward\n\t\tif iter.CurrentGlobalVersion != 0 {\n\t\t\t// dont add 1 to CurrentGlobalVersion as the index is zero based\n\t\t\tstart = iter.CurrentGlobalVersion\n\t\t}\n\t\tcursor := bucket.Cursor()\n\t\titer.tx = tx\n\t\titer.cursor = cursor\n\t\titer.startPosition = position(core.Version(start))\n\t\treturn &iter, nil\n\t\t//return &iter{tx: tx, cursor: cursor, startPosition: position(core.Version(start))}, nil\n\t}\n}\n\n// Close closes the event stream and the underlying database\nfunc (e *BBolt) Close() error {\n\treturn e.db.Close()\n}\n\n// CreateBucket creates a bucket\nfunc (e *BBolt) createBucket(bucketRef []byte, tx *bbolt.Tx) error {\n\t// Ensure that we have a bucket named event_type for the given type\n\tif _, err := tx.CreateBucketIfNotExists(bucketRef); err != nil {\n\t\treturn errors.New(fmt.Sprintf(\"could not create bucket for %s: %s\", string(bucketRef), err))\n\t}\n\treturn nil\n}\n\n// bucketRef return the reference where to store and fetch events\nfunc bucketRef(aggregateType, aggregateID string) []byte {\n\treturn []byte(aggregateType + \"_\" + aggregateID)\n}\n\n// calculate the correct posiotion and convert to bbolt key type\nfunc position(p core.Version) []byte {\n\treturn itob(uint64(p + 1))\n}\n\n// itob returns an 8-byte big endian representation of v.\nfunc itob(v uint64) []byte {\n\tb := make([]byte, 8)\n\tbinary.BigEndian.PutUint64(b, v)\n\treturn b\n}\n"
  },
  {
    "path": "eventstore/bbolt/bbolt_test.go",
    "content": "package bbolt_test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/core/testsuite\"\n\t\"github.com/hallgren/eventsourcing/eventstore/bbolt\"\n)\n\nfunc TestEventStoreSuite(t *testing.T) {\n\tf := func() (core.EventStore, func(), error) {\n\t\tdbFile := \"bolt.db\"\n\t\tes, err := bbolt.New(dbFile)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn es, func() {\n\t\t\tes.Close()\n\t\t\tos.Remove(dbFile)\n\t\t}, nil\n\t}\n\ttestsuite.Test(t, f)\n}\n\nfunc TestFetchFuncAll(t *testing.T) {\n\tdbFile := \"bolt.db\"\n\tes, err := bbolt.New(dbFile)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\tes.Close()\n\t\tos.Remove(dbFile)\n\t}()\n\n\ttestsuite.TestFetcher(t, es, es.All(0))\n}\n"
  },
  {
    "path": "eventstore/bbolt/go.mod",
    "content": "module github.com/hallgren/eventsourcing/eventstore/bbolt\n\ngo 1.23\n\ntoolchain go1.23.6\n\nrequire (\n\tgithub.com/hallgren/eventsourcing/core v0.5.2\n\tgo.etcd.io/bbolt v1.4.3\n)\n\nrequire golang.org/x/sys v0.29.0 // indirect\n\n// replace github.com/hallgren/eventsourcing/core => ../../core\n"
  },
  {
    "path": "eventstore/bbolt/go.sum",
    "content": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/hallgren/eventsourcing/core v0.5.2 h1:knvM1jP0zziiybce+Au7ysYvZQnDwxkV+/RFZWNDMiw=\ngithub.com/hallgren/eventsourcing/core v0.5.2/go.mod h1:rgo2kFwNVCb0bzUub5nOPlUYNlFkp1uUQBEQx5fM3Lk=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngo.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=\ngo.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=\ngolang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=\ngolang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=\ngolang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "eventstore/bbolt/iterator.go",
    "content": "package bbolt\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"go.etcd.io/bbolt\"\n)\n\ntype Iterator struct {\n\ttx                   *bbolt.Tx\n\tcursor               *bbolt.Cursor\n\tstartPosition        []byte\n\tvalue                []byte\n\tCurrentGlobalVersion core.Version\n}\n\n// Close closes the iterator\nfunc (i *Iterator) Close() {\n\ti.tx.Rollback()\n}\n\nfunc (i *Iterator) Next() bool {\n\tif i.cursor == nil {\n\t\treturn false\n\t}\n\t// first time Next is called go to the start position\n\tif i.value == nil {\n\t\t_, i.value = i.cursor.Seek(i.startPosition)\n\t} else {\n\t\t_, i.value = i.cursor.Next()\n\t}\n\n\tif i.value == nil {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// Next return the next event\nfunc (i *Iterator) Value() (core.Event, error) {\n\tbEvent := boltEvent{}\n\terr := json.Unmarshal(i.value, &bEvent)\n\tif err != nil {\n\t\treturn core.Event{}, errors.New(fmt.Sprintf(\"could not deserialize event, %v\", err))\n\t}\n\n\tevent := core.Event{\n\t\tAggregateID:   bEvent.AggregateID,\n\t\tAggregateType: bEvent.AggregateType,\n\t\tVersion:       core.Version(bEvent.Version),\n\t\tGlobalVersion: core.Version(bEvent.GlobalVersion),\n\t\tTimestamp:     bEvent.Timestamp,\n\t\tMetadata:      bEvent.Metadata,\n\t\tData:          bEvent.Data,\n\t\tReason:        bEvent.Reason,\n\t}\n\ti.CurrentGlobalVersion = core.Version(bEvent.GlobalVersion)\n\treturn event, nil\n}\n"
  },
  {
    "path": "eventstore/esdb/README.md",
    "content": "# esdb event store\n\nThe esdb event store is supporting the [EventStoreDB](https://www.eventstore.com) database.\n\nIt's based on the module github.com/EventStore/EventStore-Client-Go/v3 for reading and writing events.\n"
  },
  {
    "path": "eventstore/esdb/esdb.go",
    "content": "package esdb\n\nimport (\n\t\"context\"\n\n\t\"github.com/EventStore/EventStore-Client-Go/v4/esdb\"\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\nconst streamSeparator = \"-\"\n\n// ESDB is the event store handler\ntype ESDB struct {\n\tclient      *esdb.Client\n\tcontentType esdb.ContentType\n}\n\n// Open binds the event store db client\nfunc Open(client *esdb.Client, jsonSerializer bool) *ESDB {\n\t// defaults to binary\n\tvar contentType esdb.ContentType\n\tif jsonSerializer {\n\t\tcontentType = esdb.ContentTypeJson\n\t}\n\treturn &ESDB{\n\t\tclient:      client,\n\t\tcontentType: contentType,\n\t}\n}\n\n// Save persists events to the database\nfunc (es *ESDB) Save(events []core.Event) error {\n\t// If no event return no error\n\tif len(events) == 0 {\n\t\treturn nil\n\t}\n\n\tvar streamOptions esdb.AppendToStreamOptions\n\taggregateID := events[0].AggregateID\n\taggregateType := events[0].AggregateType\n\tversion := events[0].Version\n\tstream := stream(aggregateType, aggregateID)\n\tesdbEvents := make([]esdb.EventData, len(events))\n\n\tfor i, event := range events {\n\t\teventData := esdb.EventData{\n\t\t\tContentType: es.contentType,\n\t\t\tEventType:   event.Reason,\n\t\t\tData:        event.Data,\n\t\t\tMetadata:    event.Metadata,\n\t\t}\n\n\t\tesdbEvents[i] = eventData\n\t}\n\n\tif version > 1 {\n\t\t// StreamRevision value -2 due to version in the eventsourcing pkg start on 1 but in esdb on 0\n\t\t// and also the AppendToStream streamOptions expected revision is one version before the first appended event.\n\t\tstreamOptions.ExpectedRevision = esdb.StreamRevision{Value: uint64(version) - 2}\n\t} else if version == 1 {\n\t\tstreamOptions.ExpectedRevision = esdb.NoStream{}\n\t}\n\twr, err := es.client.AppendToStream(context.Background(), stream, streamOptions, esdbEvents...)\n\tif err != nil {\n\t\tif err, ok := esdb.FromError(err); !ok {\n\t\t\tif err.Code() == esdb.ErrorCodeWrongExpectedVersion {\n\t\t\t\t// return typed error if version is not the expected.\n\t\t\t\treturn core.ErrConcurrency\n\t\t\t}\n\t\t}\n\t\treturn err\n\t}\n\tfor i := range events {\n\t\t// Set all events GlobalVersion to the last events commit position.\n\t\tevents[i].GlobalVersion = core.Version(wr.CommitPosition)\n\t}\n\treturn nil\n}\n\nfunc (es *ESDB) Get(ctx context.Context, id string, aggregateType string, afterVersion core.Version) (core.Iterator, error) {\n\tstreamID := stream(aggregateType, id)\n\n\tfrom := esdb.StreamRevision{Value: uint64(afterVersion)}\n\tstream, err := es.client.ReadStream(ctx, streamID, esdb.ReadStreamOptions{From: from}, ^uint64(0))\n\tif err != nil {\n\t\tif err, ok := esdb.FromError(err); !ok {\n\t\t\tif err.Code() == esdb.ErrorCodeResourceNotFound {\n\t\t\t\treturn &Iterator{}, nil\n\t\t\t}\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn &Iterator{stream: stream}, nil\n}\n\nfunc stream(aggregateType, aggregateID string) string {\n\treturn aggregateType + streamSeparator + aggregateID\n}\n"
  },
  {
    "path": "eventstore/esdb/esdb_test.go",
    "content": "package esdb_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/EventStore/EventStore-Client-Go/v4/esdb\"\n\t\"github.com/testcontainers/testcontainers-go\"\n\t\"github.com/testcontainers/testcontainers-go/wait\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/core/testsuite\"\n\tes \"github.com/hallgren/eventsourcing/eventstore/esdb\"\n)\n\nfunc TestSuite(t *testing.T) {\n\tctx := context.Background()\n\n\treq := testcontainers.ContainerRequest{\n\t\tImage:        \"eventstore/eventstore:latest\",\n\t\tExposedPorts: []string{\"2113/tcp\"},\n\t\tWaitingFor:   wait.ForListeningPort(\"2113/tcp\"),\n\t\tCmd:          []string{\"--insecure\", \"--run-projections=All\", \"--mem-db\"},\n\t}\n\n\tcontainer, err := testcontainers.GenericContainer(\n\t\tctx,\n\t\ttestcontainers.GenericContainerRequest{\n\t\t\tContainerRequest: req,\n\t\t\tStarted:          true,\n\t\t},\n\t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tdefer container.Terminate(ctx)\n\n\tendpoint, err := container.PortEndpoint(ctx, \"2113\", \"esdb\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tf := func() (core.EventStore, func(), error) {\n\t\tsettings, err := esdb.ParseConnectionString(endpoint + \"?tls=false\")\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\n\t\tdb, err := esdb.NewClient(settings)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\n\t\tes := es.Open(db, true)\n\t\treturn es, func() {\n\t\t}, nil\n\t}\n\ttestsuite.Test(t, f)\n}\n"
  },
  {
    "path": "eventstore/esdb/go.mod",
    "content": "module github.com/hallgren/eventsourcing/eventstore/esdb\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/EventStore/EventStore-Client-Go/v4 v4.2.0\n\tgithub.com/hallgren/eventsourcing/core v0.5.2\n\tgithub.com/testcontainers/testcontainers-go v0.42.0\n)\n\nrequire (\n\tdario.cat/mergo v1.0.2 // indirect\n\tgithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/containerd/errdefs v1.0.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/containerd/log v0.1.0 // indirect\n\tgithub.com/containerd/platforms v0.2.1 // indirect\n\tgithub.com/cpuguy83/dockercfg v0.3.2 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/docker/go-connections v0.6.0 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/ebitengine/purego v0.10.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-ole/go-ole v1.3.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect\n\tgithub.com/magiconair/properties v1.8.10 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/go-archive v0.2.0 // indirect\n\tgithub.com/moby/moby/api v1.54.1 // indirect\n\tgithub.com/moby/moby/client v0.4.0 // indirect\n\tgithub.com/moby/patternmatcher v0.6.1 // indirect\n\tgithub.com/moby/sys/sequential v0.6.0 // indirect\n\tgithub.com/moby/sys/user v0.4.0 // indirect\n\tgithub.com/moby/sys/userns v0.1.0 // indirect\n\tgithub.com/moby/term v0.5.2 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/shirou/gopsutil/v4 v4.26.3 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/stretchr/testify v1.11.1 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect\n\tgo.opentelemetry.io/otel v1.41.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.41.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.41.0 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/net v0.49.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/text v0.34.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect\n\tgoogle.golang.org/grpc v1.79.3 // indirect\n\tgoogle.golang.org/protobuf v1.36.10 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n\n// replace github.com/hallgren/eventsourcing/core => ../../core\n"
  },
  {
    "path": "eventstore/esdb/go.sum",
    "content": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/EventStore/EventStore-Client-Go/v4 v4.2.0 h1:RXKiJ6pGQsWrhZ1BfKwPc1vKh8EOwNSQOXh11whk0Pw=\ngithub.com/EventStore/EventStore-Client-Go/v4 v4.2.0/go.mod h1:KSyk2r/zy2hbkbjHVqBHc0jskYmkNYmXcU5rhMOlWKg=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=\ngithub.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=\ngithub.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=\ngithub.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=\ngithub.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=\ngithub.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40=\ngithub.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4=\ngithub.com/hallgren/eventsourcing/core v0.5.2 h1:knvM1jP0zziiybce+Au7ysYvZQnDwxkV+/RFZWNDMiw=\ngithub.com/hallgren/eventsourcing/core v0.5.2/go.mod h1:rgo2kFwNVCb0bzUub5nOPlUYNlFkp1uUQBEQx5fM3Lk=\ngithub.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=\ngithub.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=\ngithub.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=\ngithub.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=\ngithub.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=\ngithub.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=\ngithub.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=\ngithub.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=\ngithub.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=\ngithub.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=\ngithub.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=\ngithub.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=\ngithub.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=\ngithub.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=\ngithub.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=\ngithub.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=\ngithub.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=\ngithub.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=\ngithub.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=\ngithub.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=\ngithub.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=\ngithub.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=\ngithub.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=\ngithub.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=\ngithub.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=\ngithub.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=\ngo.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=\ngo.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=\ngo.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=\ngo.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=\ngo.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=\ngo.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=\ngo.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=\ngo.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=\ngolang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=\ngoogle.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=\ngoogle.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=\ngoogle.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\npgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=\npgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=\n"
  },
  {
    "path": "eventstore/esdb/iterator.go",
    "content": "package esdb\n\nimport (\n\t\"strings\"\n\n\t\"github.com/EventStore/EventStore-Client-Go/v4/esdb\"\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\ntype Iterator struct {\n\tstream *esdb.ReadStream\n\tevent  *esdb.ResolvedEvent\n}\n\n// Close closes the stream\nfunc (i *Iterator) Close() {\n\ti.stream.Close()\n}\n\n// Next steps to the next event in the stream\nfunc (i *Iterator) Next() bool {\n\tif i.stream == nil {\n\t\treturn false\n\t}\n\teventESDB, err := i.stream.Recv()\n\tif err != nil {\n\t\treturn false\n\t}\n\ti.event = eventESDB\n\treturn true\n}\n\n// Value returns the event from the stream\nfunc (i *Iterator) Value() (core.Event, error) {\n\tstream := strings.Split(i.event.Event.StreamID, streamSeparator)\n\n\tevent := core.Event{\n\t\tAggregateID:   stream[1],\n\t\tVersion:       core.Version(i.event.Event.EventNumber) + 1, // +1 as the eventsourcing Version starts on 1 but the esdb event version starts on 0\n\t\tAggregateType: stream[0],\n\t\tTimestamp:     i.event.Event.CreatedDate,\n\t\tData:          i.event.Event.Data,\n\t\tMetadata:      i.event.Event.UserMetadata,\n\t\tReason:        i.event.Event.EventType,\n\t\t// Can't get the global version when using the ReadStream method\n\t\t//GlobalVersion: core.Version(event.Event.Position.Commit),\n\t}\n\treturn event, nil\n}\n"
  },
  {
    "path": "eventstore/kurrent/go.mod",
    "content": "module github.com/hallgren/eventsourcing/eventstore/kurrent\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/hallgren/eventsourcing/core v0.5.2\n\tgithub.com/kurrent-io/KurrentDB-Client-Go v1.1.2\n\tgithub.com/testcontainers/testcontainers-go v0.42.0\n)\n\nrequire (\n\tdario.cat/mergo v1.0.2 // indirect\n\tgithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/containerd/errdefs v1.0.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/containerd/log v0.1.0 // indirect\n\tgithub.com/containerd/platforms v0.2.1 // indirect\n\tgithub.com/cpuguy83/dockercfg v0.3.2 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/docker/go-connections v0.6.0 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/ebitengine/purego v0.10.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-ole/go-ole v1.3.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect\n\tgithub.com/magiconair/properties v1.8.10 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/go-archive v0.2.0 // indirect\n\tgithub.com/moby/moby/api v1.54.1 // indirect\n\tgithub.com/moby/moby/client v0.4.0 // indirect\n\tgithub.com/moby/patternmatcher v0.6.1 // indirect\n\tgithub.com/moby/sys/sequential v0.6.0 // indirect\n\tgithub.com/moby/sys/user v0.4.0 // indirect\n\tgithub.com/moby/sys/userns v0.1.0 // indirect\n\tgithub.com/moby/term v0.5.2 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/shirou/gopsutil/v4 v4.26.3 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/stretchr/testify v1.11.1 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect\n\tgo.opentelemetry.io/otel v1.41.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.41.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.41.0 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/net v0.49.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgolang.org/x/text v0.34.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect\n\tgoogle.golang.org/grpc v1.79.3 // indirect\n\tgoogle.golang.org/protobuf v1.36.10 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n\n// replace github.com/hallgren/eventsourcing/core => ../../core\n"
  },
  {
    "path": "eventstore/kurrent/go.sum",
    "content": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=\ngithub.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=\ngithub.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=\ngithub.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=\ngithub.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=\ngithub.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hallgren/eventsourcing/core v0.5.2 h1:knvM1jP0zziiybce+Au7ysYvZQnDwxkV+/RFZWNDMiw=\ngithub.com/hallgren/eventsourcing/core v0.5.2/go.mod h1:rgo2kFwNVCb0bzUub5nOPlUYNlFkp1uUQBEQx5fM3Lk=\ngithub.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kurrent-io/KurrentDB-Client-Go v1.1.2 h1:Pua/rb8eMbmb4lS3uhf42b6StAVJouA9GD6U611EiM8=\ngithub.com/kurrent-io/KurrentDB-Client-Go v1.1.2/go.mod h1:ddlaMCBJtaKn6gjzjilE4eZ+upG6Aqz/R4tamYTHBTE=\ngithub.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=\ngithub.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=\ngithub.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=\ngithub.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=\ngithub.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=\ngithub.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=\ngithub.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=\ngithub.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=\ngithub.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=\ngithub.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=\ngithub.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=\ngithub.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=\ngithub.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=\ngithub.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=\ngithub.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=\ngithub.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=\ngithub.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=\ngithub.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=\ngithub.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=\ngithub.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=\ngithub.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=\ngithub.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=\ngithub.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=\ngithub.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=\ngithub.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=\ngithub.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=\ngo.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=\ngo.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=\ngo.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=\ngo.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=\ngo.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=\ngo.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=\ngo.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=\ngo.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=\ngolang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=\ngoogle.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=\ngoogle.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\ngoogle.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=\ngoogle.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\npgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=\npgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=\n"
  },
  {
    "path": "eventstore/kurrent/iterator.go",
    "content": "package kurrent\n\nimport (\n\t\"strings\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/kurrent-io/KurrentDB-Client-Go/kurrentdb\"\n)\n\ntype Iterator struct {\n\tStream *kurrentdb.ReadStream\n\tevent  *kurrentdb.ResolvedEvent\n}\n\n// Close closes the stream\nfunc (i *Iterator) Close() {\n\ti.Stream.Close()\n}\n\n// Next steps to the next event in the stream\nfunc (i *Iterator) Next() bool {\n\tif i.Stream == nil {\n\t\treturn false\n\t}\n\tevent, err := i.Stream.Recv()\n\tif err != nil {\n\t\treturn false\n\t}\n\ti.event = event\n\treturn true\n}\n\n// Value returns the event from the stream\nfunc (i *Iterator) Value() (core.Event, error) {\n\tstream := strings.Split(i.event.Event.StreamID, streamSeparator)\n\n\tevent := core.Event{\n\t\tAggregateID:   stream[1],\n\t\tVersion:       core.Version(i.event.Event.EventNumber) + 1, // +1 as the eventsourcing Version starts on 1 but the kurrent event version starts on 0\n\t\tAggregateType: stream[0],\n\t\tTimestamp:     i.event.Event.CreatedDate,\n\t\tData:          i.event.Event.Data,\n\t\tMetadata:      i.event.Event.UserMetadata,\n\t\tReason:        i.event.Event.EventType,\n\t\t// Can't get the global version when using the ReadStream method\n\t\t//GlobalVersion: core.Version(event.Event.Position.Commit),\n\t}\n\treturn event, nil\n}\n"
  },
  {
    "path": "eventstore/kurrent/kurrent.go",
    "content": "package kurrent\n\nimport (\n\t\"context\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/kurrent-io/KurrentDB-Client-Go/kurrentdb\"\n)\n\nconst streamSeparator = \"-\"\n\n// Kurrent is the event store handler\ntype Kurrent struct {\n\tclient      *kurrentdb.Client\n\tcontentType kurrentdb.ContentType\n}\n\n// Open binds the event store db client\nfunc Open(client *kurrentdb.Client, jsonSerializer bool) *Kurrent {\n\t// defaults to binary\n\tvar contentType kurrentdb.ContentType\n\tif jsonSerializer {\n\t\tcontentType = kurrentdb.ContentTypeJson\n\t}\n\treturn &Kurrent{\n\t\tclient:      client,\n\t\tcontentType: contentType,\n\t}\n}\n\n// Save persists events to the database\nfunc (es *Kurrent) Save(events []core.Event) error {\n\t// If no event return no error\n\tif len(events) == 0 {\n\t\treturn nil\n\t}\n\n\tvar streamOptions kurrentdb.AppendToStreamOptions\n\taggregateID := events[0].AggregateID\n\taggregateType := events[0].AggregateType\n\tversion := events[0].Version\n\tstream := stream(aggregateType, aggregateID)\n\tKurrentEvents := make([]kurrentdb.EventData, len(events))\n\n\tfor i, event := range events {\n\t\teventData := kurrentdb.EventData{\n\t\t\tContentType: es.contentType,\n\t\t\tEventType:   event.Reason,\n\t\t\tData:        event.Data,\n\t\t\tMetadata:    event.Metadata,\n\t\t}\n\n\t\tKurrentEvents[i] = eventData\n\t}\n\n\tif version > 1 {\n\t\t// StreamRevision value -2 due to version in the eventsourcing pkg start on 1 but in kurrent on 0\n\t\t// and also the AppendToStream streamOptions expected revision is one version before the first appended event.\n\t\tstreamOptions.StreamState = kurrentdb.StreamRevision{Value: uint64(version) - 2}\n\t} else if version == 1 {\n\t\tstreamOptions.StreamState = kurrentdb.NoStream{}\n\t}\n\twr, err := es.client.AppendToStream(context.Background(), stream, streamOptions, KurrentEvents...)\n\tif err != nil {\n\t\tif err, ok := kurrentdb.FromError(err); !ok {\n\t\t\tif err.Code() == kurrentdb.ErrorCodeWrongExpectedVersion {\n\t\t\t\t// return typed error if version is not the expected.\n\t\t\t\treturn core.ErrConcurrency\n\t\t\t}\n\t\t}\n\t\treturn err\n\t}\n\tfor i := range events {\n\t\t// Set all events GlobalVersion to the last events commit position.\n\t\tevents[i].GlobalVersion = core.Version(wr.CommitPosition)\n\t}\n\treturn nil\n}\n\nfunc (es *Kurrent) Get(ctx context.Context, id string, aggregateType string, afterVersion core.Version) (core.Iterator, error) {\n\tstreamID := stream(aggregateType, id)\n\n\tfrom := kurrentdb.StreamRevision{Value: uint64(afterVersion)}\n\tstream, err := es.client.ReadStream(ctx, streamID, kurrentdb.ReadStreamOptions{From: from}, ^uint64(0))\n\tif err != nil {\n\t\tif err, ok := kurrentdb.FromError(err); !ok {\n\t\t\tif err.Code() == kurrentdb.ErrorCodeResourceNotFound {\n\t\t\t\treturn &Iterator{}, nil\n\t\t\t}\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn &Iterator{Stream: stream}, nil\n}\n\nfunc stream(aggregateType, aggregateID string) string {\n\treturn aggregateType + streamSeparator + aggregateID\n}\n"
  },
  {
    "path": "eventstore/kurrent/kurrent_test.go",
    "content": "package kurrent_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/kurrent-io/KurrentDB-Client-Go/kurrentdb\"\n\t\"github.com/testcontainers/testcontainers-go\"\n\t\"github.com/testcontainers/testcontainers-go/wait\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/core/testsuite\"\n\t\"github.com/hallgren/eventsourcing/eventstore/kurrent\"\n)\n\nfunc TestSuite(t *testing.T) {\n\tctx := context.Background()\n\n\treq := testcontainers.ContainerRequest{\n\t\tImage:        \"kurrentplatform/kurrentdb:latest\",\n\t\tExposedPorts: []string{\"2113/tcp\"},\n\t\tWaitingFor:   wait.ForListeningPort(\"2113/tcp\"),\n\t\tCmd:          []string{\"--insecure\", \"--run-projections=All\", \"--mem-db\"},\n\t}\n\n\tcontainer, err := testcontainers.GenericContainer(\n\t\tctx,\n\t\ttestcontainers.GenericContainerRequest{\n\t\t\tContainerRequest: req,\n\t\t\tStarted:          true,\n\t\t},\n\t)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tdefer container.Terminate(ctx)\n\n\tendpoint, err := container.PortEndpoint(ctx, \"2113\", \"kurrent\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tf := func() (core.EventStore, func(), error) {\n\t\tsettings, err := kurrentdb.ParseConnectionString(endpoint + \"?tls=false\")\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\n\t\tdb, err := kurrentdb.NewClient(settings)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\n\t\tes := kurrent.Open(db, true)\n\t\treturn es, func() {\n\t\t}, nil\n\t}\n\ttestsuite.Test(t, f)\n}\n"
  },
  {
    "path": "eventstore/memory/iterator.go",
    "content": "package memory\n\nimport \"github.com/hallgren/eventsourcing/core\"\n\ntype iterator struct {\n\tevents   []core.Event\n\tposition int\n\tevent    core.Event\n}\n\nfunc (i *iterator) Next() bool {\n\tif len(i.events) <= i.position {\n\t\treturn false\n\t}\n\ti.event = i.events[i.position]\n\ti.position++\n\treturn true\n}\n\nfunc (i *iterator) Value() (core.Event, error) {\n\treturn i.event, nil\n}\n\nfunc (i *iterator) Close() {\n\ti.events = nil\n\ti.position = 0\n}\n"
  },
  {
    "path": "eventstore/memory/memory.go",
    "content": "package memory\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\n// Memory is a handler for event streaming\ntype Memory struct {\n\taggregateEvents map[string][]core.Event // The memory structure where we store aggregate events\n\teventsInOrder   []core.Event            // The global event order\n\tlock            sync.Mutex\n}\n\n// Create in memory event store\nfunc Create() *Memory {\n\treturn &Memory{\n\t\taggregateEvents: make(map[string][]core.Event),\n\t\teventsInOrder:   make([]core.Event, 0),\n\t}\n}\n\n// Save an aggregate (its events)\nfunc (e *Memory) Save(events []core.Event) error {\n\t// Return if there is no events to save\n\tif len(events) == 0 {\n\t\treturn nil\n\t}\n\n\t// make sure its thread safe\n\te.lock.Lock()\n\tdefer e.lock.Unlock()\n\n\t// get bucket name from first event\n\taggregateType := events[0].AggregateType\n\taggregateID := events[0].AggregateID\n\tbucketName := aggregateKey(aggregateType, aggregateID)\n\n\tevBucket := e.aggregateEvents[bucketName]\n\tcurrentVersion := core.Version(0)\n\n\tif len(evBucket) > 0 {\n\t\t// Last version in the list\n\t\tlastEvent := evBucket[len(evBucket)-1]\n\t\tcurrentVersion = lastEvent.Version\n\t}\n\n\t// Make sure no other has saved event to the same aggregate concurrently\n\tif core.Version(currentVersion)+1 != events[0].Version {\n\t\treturn core.ErrConcurrency\n\t}\n\n\tfor i, event := range events {\n\t\t// set the global version on the event +1 as if the event was already on the eventsInOrder slice\n\t\tevent.GlobalVersion = core.Version(len(e.eventsInOrder) + 1)\n\t\tevBucket = append(evBucket, event)\n\t\te.eventsInOrder = append(e.eventsInOrder, event)\n\t\t// override the event in the slice exposing the GlobalVersion to the caller\n\t\tevents[i].GlobalVersion = event.GlobalVersion\n\t}\n\n\te.aggregateEvents[bucketName] = evBucket\n\treturn nil\n}\n\n// Get aggregate events\nfunc (e *Memory) Get(ctx context.Context, id string, aggregateType string, afterVersion core.Version) (core.Iterator, error) {\n\tvar events []core.Event\n\t// make sure its thread safe\n\te.lock.Lock()\n\tdefer e.lock.Unlock()\n\n\tfor _, e := range e.aggregateEvents[aggregateKey(aggregateType, id)] {\n\t\tif e.Version > afterVersion {\n\t\t\tevents = append(events, e)\n\t\t}\n\t}\n\treturn &iterator{events: events}, ctx.Err()\n}\n\n// Close does nothing\nfunc (e *Memory) Close() {}\n\n// aggregateKey generates a key to store events against from aggregateType and aggregateID\nfunc aggregateKey(aggregateType, aggregateID string) string {\n\treturn aggregateType + \"_\" + aggregateID\n}\n\n// globalEvents returns count events in order globally from the start position\nfunc (e *Memory) globalEvents(start core.Version, count uint64) ([]core.Event, error) {\n\tevents := make([]core.Event, 0, count)\n\t// make sure its thread safe\n\te.lock.Lock()\n\tdefer e.lock.Unlock()\n\n\tfor _, e := range e.eventsInOrder {\n\t\t// find start position and append until counter is 0\n\t\tif e.GlobalVersion >= start {\n\t\t\tevents = append(events, e)\n\t\t\tcount--\n\t\t\tif count == 0 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn events, nil\n}\n\n// All iterate over all events in GlobalEvents order\nfunc (m *Memory) All(start core.Version, count uint64) core.Fetcher {\n\treturn func() (core.Iterator, error) {\n\t\tevents, err := m.globalEvents(start, count)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// no events to fetch\n\t\tif len(events) == 0 {\n\t\t\treturn &iterator{events: []core.Event{}}, nil\n\t\t}\n\n\t\t// next time the function is called it will start from the last fetched event +1\n\t\tstart = events[len(events)-1].GlobalVersion + 1\n\t\treturn &iterator{events: events}, nil\n\t}\n}\n"
  },
  {
    "path": "eventstore/memory/memory_test.go",
    "content": "package memory_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/core/testsuite\"\n\t\"github.com/hallgren/eventsourcing/eventstore/memory\"\n)\n\nfunc TestEventStore(t *testing.T) {\n\tf := func() (core.EventStore, func(), error) {\n\t\tes := memory.Create()\n\t\treturn es, func() { es.Close() }, nil\n\t}\n\ttestsuite.Test(t, f)\n}\n\nfunc TestFetcherAll(t *testing.T) {\n\tes := memory.Create()\n\tdefer es.Close()\n\ttestsuite.TestFetcher(t, es, es.All(0, 10))\n}\n"
  },
  {
    "path": "eventstore/sql/README.md",
    "content": "# SQL Event Store\n\nThe sql eventstore is a module containing multiple sql based event stores that are all based on the\ndatabase/sql interface in go standard library. The different event stores has specific database schemas\nthat support the different databases.\n\n## SQLite\n\nSupports the SQLite database https://www.sqlite.org/\n\n### Database Schema\n\n```go\n    CREATE TABLE IF NOT EXISTS events (\n        seq        INTEGER PRIMARY KEY AUTOINCREMENT,\n        id         VARCHAR NOT NULL,\n        version    INTEGER,\n        reason     VARCHAR,\n        type       VARCHAR,\n        timestamp  VARCHAR,\n        data       BLOB,\n        metadata   BLOB,\n        UNIQUE (id, type, version)\n    );\n\n    CREATE INDEX IF NOT EXISTS id_type ON events (id, type);\n```\n\n### Constructor\n\n```go\n// NewSQLite connection to database\nNewSQLite(db *sql.DB) (*SQLite, error) \n\n// NewSQLiteSingelWriter prevents multiple writers to save events concurrently\n//\n// Multiple go routines writing concurrently to sqlite could produce sqlite to lock.\n// https://www.sqlite.org/cgi/src/doc/begin-concurrent/doc/begin_concurrent.md\n//\n// \"If there is significant contention for the writer lock, this mechanism can\n// be inefficient. In this case it is better for the application to use a mutex\n// or some other mechanism that supports blocking to ensure that at most one\n// writer is attempting to COMMIT a BEGIN CONCURRENT transaction at a time.\n// This is usually easier if all writers are part of the same operating system process.\"\nNewSQLiteSingelWriter(db *sql.DB) (*SQLite, error)\n```\n\n### Example of use\n\n```go\nimport (\n\t// have to alias the sql package as it use the same name\n\tgosql \"database/sql\"\n\t\"github.com/hallgren/eventsourcing/eventstore/sql\"\n\t// use the sqlite driver from mattn in this example\n\t_ \"github.com/mattn/go-sqlite3\"\n)\n\ndb, err := gosql.Open(\"sqlite3\", \"file::memory:?cache=shared\")\nif err != nil {\n\treturn nil, nil, errors.New(fmt.Sprintf(\"could not open database %v\", err))\n}\nerr = db.Ping()\nif err != nil {\n\treturn nil, nil, errors.New(fmt.Sprintf(\"could not ping database %v\", err))\n}\nsqliteEventStore, err := sql.NewSQLiteSingelWriter(db)\nif err != nil {\n\treturn nil, nil, err\n}\n```\n\n## Postgres\n\nSupports the Postgres database https://www.postgresql.org\n\n### Database Schema\n\n```go\nCREATE TABLE IF NOT EXISTS events (\n\tseq SERIAL PRIMARY KEY,\n\tid VARCHAR NOT NULL,\n\tversion INTEGER,\n\treason VARCHAR,\n\ttype VARCHAR,\n\ttimestamp VARCHAR,\n\tdata BYTEA,\n\tmetadata BYTEA,\n\tUNIQUE (id, type, version)\n);\n\nCREATE INDEX IF NOT EXISTS id_type ON events (id, type);\n```\n\n### Constructor\n\n```go\n// NewPostgres connection to database\nfunc NewPostgres(db *sql.DB) (*Postgres, error) {\n```\n\n### Example of use\n\n```go\nimport (\n\t// have to alias the sql package as it use the same name\n\tgosql \"database/sql\"\n\t\n\t// in this example we use the pg postgres driver\n\t_ \"github.com/lib/pq\"\n\t\"github.com/hallgren/eventsourcing/eventstore/sql\"\n)\n\ndb, err := gosql.Open(\"postgres\", dsn)\nif err != nil {\n\treturn nil, nil, fmt.Errorf(\"db open failed: %w\", err)\n}\n// Test the connection\nerr = db.Ping()\nif err != nil {\n\treturn nil, nil, err\n}\npostgresEventStore, err := sql.NewPostgres(db)\n```\n\n## Microsoft SQL Server\n\nSupports Microsoft SQL Server database https://www.microsoft.com/en-us/sql-server\n\n### Database Schema\n\n```go\nIF OBJECT_ID('[events]', 'U') IS NULL\nBEGIN\n    CREATE TABLE [events] (\n        [seq] INT IDENTITY(1,1) PRIMARY KEY,\n        [id] NVARCHAR(255) NOT NULL,\n        [version] INT,\n        [reason] NVARCHAR(255),\n        [type] NVARCHAR(255),\n        [timestamp] NVARCHAR(255),\n        [data] VARBINARY(MAX),\n        [metadata] VARBINARY(MAX),\n        CONSTRAINT uq_events UNIQUE ([id], [type], [version])\n    );\nEND\n\nIF NOT EXISTS (\n    SELECT 1 \n    FROM sys.indexes \n    WHERE name = 'id_type' AND object_id = OBJECT_ID('events')\n)\nBEGIN\n    CREATE INDEX id_type ON [events] ([id], [type]);\nEND\n\nIF NOT EXISTS (\n    SELECT 1 \n    FROM sys.indexes \n    WHERE name = 'id_type' AND object_id = OBJECT_ID('events')\n)\nBEGIN\n    CREATE INDEX id_type ON [events] ([id], [type]);\nEND\n```\n\n### Constructor\n\n```go\n// NewSQLServer connection to database\nfunc NewSQLServer(db *sql.DB) (*SQLServer, error) {\n```\n\n### Example of use\n\n```go\nimport (\n\t// alias the go sql package\n\tgosql \"database/sql\"\n\t // uses the sql server driver from denisenkom\n\t_ \"github.com/denisenkom/go-mssqldb\"\n\t\"github.com/hallgren/eventsourcing/eventstore/sql\"\n)\n\ndb, err := gosql.Open(\"sqlserver\", dsn)\nif err != nil {\n\treturn err\n}\nerr = db.Ping() {\nif err != nil {\n\treturn err\n}\nSqlServerEventStore, err := sql.NewSQLServer(db)\n```\n"
  },
  {
    "path": "eventstore/sql/go.mod",
    "content": "module github.com/hallgren/eventsourcing/eventstore/sql\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/denisenkom/go-mssqldb v0.12.3\n\tgithub.com/hallgren/eventsourcing/core v0.5.2\n\tgithub.com/lib/pq v1.12.3\n\tgithub.com/mattn/go-sqlite3 v1.14.42\n\tgithub.com/testcontainers/testcontainers-go v0.42.0\n)\n\nrequire (\n\tdario.cat/mergo v1.0.2 // indirect\n\tgithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/containerd/errdefs v1.0.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/containerd/log v0.1.0 // indirect\n\tgithub.com/containerd/platforms v0.2.1 // indirect\n\tgithub.com/cpuguy83/dockercfg v0.3.2 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/docker/go-connections v0.6.0 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/ebitengine/purego v0.10.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-ole/go-ole v1.2.6 // indirect\n\tgithub.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect\n\tgithub.com/golang-sql/sqlexp v0.1.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect\n\tgithub.com/magiconair/properties v1.8.10 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/go-archive v0.2.0 // indirect\n\tgithub.com/moby/moby/api v1.54.1 // indirect\n\tgithub.com/moby/moby/client v0.4.0 // indirect\n\tgithub.com/moby/patternmatcher v0.6.1 // indirect\n\tgithub.com/moby/sys/sequential v0.6.0 // indirect\n\tgithub.com/moby/sys/user v0.4.0 // indirect\n\tgithub.com/moby/sys/userns v0.1.0 // indirect\n\tgithub.com/moby/term v0.5.2 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/shirou/gopsutil/v4 v4.26.3 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/stretchr/testify v1.11.1 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect\n\tgo.opentelemetry.io/otel v1.41.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.41.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.41.0 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n\n// replace github.com/hallgren/eventsourcing/core => ../../core\n"
  },
  {
    "path": "eventstore/sql/go.sum",
    "content": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=\ngithub.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=\ngithub.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=\ngithub.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=\ngithub.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=\ngithub.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=\ngithub.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=\ngithub.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=\ngithub.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=\ngithub.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=\ngithub.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=\ngithub.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hallgren/eventsourcing/core v0.5.2 h1:knvM1jP0zziiybce+Au7ysYvZQnDwxkV+/RFZWNDMiw=\ngithub.com/hallgren/eventsourcing/core v0.5.2/go.mod h1:rgo2kFwNVCb0bzUub5nOPlUYNlFkp1uUQBEQx5fM3Lk=\ngithub.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=\ngithub.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=\ngithub.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=\ngithub.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=\ngithub.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=\ngithub.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=\ngithub.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=\ngithub.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=\ngithub.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=\ngithub.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=\ngithub.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=\ngithub.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=\ngithub.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=\ngithub.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=\ngithub.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=\ngithub.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=\ngithub.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=\ngithub.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=\ngithub.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=\ngithub.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=\ngithub.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=\ngithub.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=\ngithub.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=\ngithub.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=\ngithub.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=\ngithub.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=\ngithub.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=\ngithub.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=\ngithub.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=\ngithub.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=\ngo.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=\ngo.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=\ngo.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=\ngo.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=\ngo.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=\ngo.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=\ngo.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=\ngo.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=\ngo.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=\ngo.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\npgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=\npgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=\n"
  },
  {
    "path": "eventstore/sql/iterator.go",
    "content": "package sql\n\nimport (\n\t\"database/sql\"\n\t\"time\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\ntype Iterator struct {\n\tRows                 *sql.Rows\n\tCurrentGlobalVersion core.Version\n}\n\n// Next return true if there are more data\nfunc (i *Iterator) Next() bool {\n\treturn i.Rows.Next()\n}\n\n// Value return the an event\nfunc (i *Iterator) Value() (core.Event, error) {\n\tvar globalVersion core.Version\n\tvar version core.Version\n\tvar id, reason, typ, timestamp string\n\tvar data, metadata []byte\n\n\tif err := i.Rows.Scan(&globalVersion, &id, &version, &reason, &typ, &timestamp, &data, &metadata); err != nil {\n\t\treturn core.Event{}, err\n\t}\n\n\tt, err := time.Parse(time.RFC3339, timestamp)\n\tif err != nil {\n\t\treturn core.Event{}, err\n\t}\n\n\tevent := core.Event{\n\t\tAggregateID:   id,\n\t\tVersion:       version,\n\t\tGlobalVersion: globalVersion,\n\t\tAggregateType: typ,\n\t\tTimestamp:     t,\n\t\tData:          data,\n\t\tMetadata:      metadata,\n\t\tReason:        reason,\n\t}\n\ti.CurrentGlobalVersion = globalVersion\n\treturn event, nil\n}\n\n// Close closes the iterator\nfunc (i *Iterator) Close() {\n\ti.Rows.Close()\n}\n"
  },
  {
    "path": "eventstore/sql/migrate.go",
    "content": "package sql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n)\n\nfunc migrate(db *sql.DB, stm []string) error {\n\ttx, err := db.BeginTx(context.Background(), nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer tx.Rollback()\n\n\tfor _, b := range stm {\n\t\t_, err := tx.Exec(b)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn tx.Commit()\n}\n"
  },
  {
    "path": "eventstore/sql/postgres.go",
    "content": "package sql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\nvar postgresStm = []string{`CREATE TABLE IF NOT EXISTS events (\n\tseq SERIAL PRIMARY KEY,\n\tid VARCHAR NOT NULL,\n\tversion INTEGER,\n\treason VARCHAR,\n\ttype VARCHAR,\n\ttimestamp VARCHAR,\n\tdata BYTEA,\n\tmetadata BYTEA,\n\tUNIQUE (id, type, version)\n);`,\n\t`CREATE INDEX IF NOT EXISTS id_type ON events (id, type);`,\n}\n\n// Postgres event store handler\ntype Postgres struct {\n\tdb   *sql.DB\n\tlock *sync.Mutex\n}\n\n// NewPostgres connection to database\nfunc NewPostgres(db *sql.DB) (*Postgres, error) {\n\t// make sure the schema is migrated\n\tif err := migrate(db, postgresStm); err != nil {\n\t\treturn nil, err\n\t}\n\ts := &Postgres{\n\t\tdb: db,\n\t}\n\treturn s, nil\n}\n\n// Close the connection\nfunc (s *Postgres) Close() {\n\ts.db.Close()\n}\n\n// Save persists events to the database\nfunc (s *Postgres) Save(events []core.Event) error {\n\t// If no event return no error\n\tif len(events) == 0 {\n\t\treturn nil\n\t}\n\n\tif s.lock != nil {\n\t\t// prevent multiple writers\n\t\ts.lock.Lock()\n\t\tdefer s.lock.Unlock()\n\t}\n\taggregateID := events[0].AggregateID\n\taggregateType := events[0].AggregateType\n\n\ttx, err := s.db.BeginTx(context.Background(), nil)\n\tif err != nil {\n\t\treturn errors.New(fmt.Sprintf(\"could not start a write transaction, %v\", err))\n\t}\n\tdefer tx.Rollback()\n\n\tvar currentVersion core.Version\n\tvar version int\n\tselectStm := `SELECT version FROM events WHERE id=$1 and type=$2 ORDER BY version DESC LIMIT 1`\n\terr = tx.QueryRow(selectStm, aggregateID, aggregateType).Scan(&version)\n\tif err != nil && err != sql.ErrNoRows {\n\t\treturn err\n\t} else if err == sql.ErrNoRows {\n\t\t// if no events are saved before set the current version to zero\n\t\tcurrentVersion = core.Version(0)\n\t} else {\n\t\t// set the current version to the last event stored\n\t\tcurrentVersion = core.Version(version)\n\t}\n\n\t// Make sure no other has saved event to the same aggregate concurrently\n\tif core.Version(currentVersion)+1 != events[0].Version {\n\t\treturn core.ErrConcurrency\n\t}\n\n\tvar lastInsertedID int64\n\tinsert := `INSERT INTO events (id, version, reason, type, timestamp, data, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING seq`\n\tfor i, event := range events {\n\t\terr := tx.QueryRow(insert, event.AggregateID, event.Version, event.Reason, event.AggregateType, event.Timestamp.Format(time.RFC3339), event.Data, event.Metadata).Scan(&lastInsertedID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// override the event in the slice exposing the GlobalVersion to the caller\n\t\tevents[i].GlobalVersion = core.Version(lastInsertedID)\n\t}\n\treturn tx.Commit()\n}\n\n// Get the events from database\nfunc (s *Postgres) Get(ctx context.Context, id string, aggregateType string, afterVersion core.Version) (core.Iterator, error) {\n\tselectStm := `SELECT seq, id, version, reason, type, timestamp, data, metadata FROM events WHERE id=$1 AND type=$2 AND version>$3 ORDER BY version ASC`\n\trows, err := s.db.QueryContext(ctx, selectStm, id, aggregateType, afterVersion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Iterator{Rows: rows}, nil\n}\n\n// All iterate over all event in GlobalEvents order\nfunc (s *Postgres) All(start core.Version) core.Fetcher {\n\titer := Iterator{}\n\treturn func() (core.Iterator, error) {\n\t\t// set start from second call and forward\n\t\tif iter.CurrentGlobalVersion != 0 {\n\t\t\tstart = iter.CurrentGlobalVersion + 1\n\t\t}\n\t\tselectStm := `SELECT seq, id, version, reason, type, timestamp, data, metadata FROM events WHERE seq >= $1 ORDER BY seq ASC`\n\t\trows, err := s.db.Query(selectStm, start)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\titer.Rows = rows\n\t\treturn &iter, nil\n\t}\n}\n"
  },
  {
    "path": "eventstore/sql/postgres_test.go",
    "content": "package sql_test\n\nimport (\n\t\"context\"\n\tgosql \"database/sql\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t_ \"github.com/lib/pq\"\n\t\"github.com/testcontainers/testcontainers-go\"\n\t\"github.com/testcontainers/testcontainers-go/wait\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/core/testsuite\"\n\t\"github.com/hallgren/eventsourcing/eventstore/sql\"\n)\n\nfunc TestSuitePostgres(t *testing.T) {\n\tdsn, closer, err := postgresServer()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer closer()\n\n\tf := func() (core.EventStore, func(), error) {\n\t\tes, err := postgreConnect(dsn)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn es, es.Close, nil\n\t}\n\ttestsuite.Test(t, f)\n}\n\nfunc TestFetcherAllPostgres(t *testing.T) {\n\tdsn, closer, err := postgresServer()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer closer()\n\tes, err := postgreConnect(dsn)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer es.Close()\n\ttestsuite.TestFetcher(t, es, es.All(0))\n}\n\nfunc postgresServer() (string, func(), error) {\n\tctx := context.Background()\n\n\t// Set up the PostgreSQL container request\n\treq := testcontainers.ContainerRequest{\n\t\tImage:        \"postgres:16\", // Use a specific version\n\t\tExposedPorts: []string{\"5432/tcp\"},\n\t\tEnv: map[string]string{\n\t\t\t\"POSTGRES_USER\":     \"test\",\n\t\t\t\"POSTGRES_PASSWORD\": \"secret\",\n\t\t\t\"POSTGRES_DB\":       \"testdb\",\n\t\t},\n\t\tWaitingFor: wait.ForListeningPort(\"5432/tcp\").WithStartupTimeout(30 * time.Second),\n\t}\n\t// Start the container\n\tpostgresContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{\n\t\tContainerRequest: req,\n\t\tStarted:          true,\n\t})\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\t// Get container host and port\n\thost, _ := postgresContainer.Host(ctx)\n\tport, _ := postgresContainer.MappedPort(ctx, \"5432\")\n\n\t// Build the DSN\n\tdsn := fmt.Sprintf(\"host=%s port=%s user=test password=secret dbname=testdb sslmode=disable\", host, port.Port())\n\treturn dsn, func() { postgresContainer.Terminate(ctx) }, nil\n\n}\n\nfunc postgreConnect(dsn string) (*sql.Postgres, error) {\n\t// Connect using database/sql\n\tdb, err := gosql.Open(\"postgres\", dsn)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"db open failed: %w\", err)\n\t}\n\t// Test the connection\n\terr = db.Ping()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn sql.NewPostgres(db)\n}\n"
  },
  {
    "path": "eventstore/sql/sqlite.go",
    "content": "package sql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\nvar stm = []string{\n\t`CREATE TABLE IF NOT EXISTS events (\n\t\tseq        INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\tid         VARCHAR NOT NULL,\n\t\tversion    INTEGER,\n\t\treason     VARCHAR,\n\t\ttype       VARCHAR,\n\t\ttimestamp  VARCHAR,\n\t\tdata       BLOB,\n\t\tmetadata   BLOB,\n\t\tUNIQUE (id, type, version)\n\t);`,\n\t`CREATE INDEX IF NOT EXISTS id_type ON events (id, type);`,\n}\n\n// SQLite event store handler\ntype SQLite struct {\n\tdb   *sql.DB\n\tlock *sync.Mutex\n}\n\n// NewSQLite connection to database\nfunc NewSQLite(db *sql.DB) (*SQLite, error) {\n\tif err := migrate(db, stm); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &SQLite{\n\t\tdb: db,\n\t}, nil\n}\n\n// NewSQLiteSingelWriter prevents multiple writers to save events concurrently\n//\n// Multiple go routines writing concurrently to sqlite could produce sqlite to lock.\n// https://www.sqlite.org/cgi/src/doc/begin-concurrent/doc/begin_concurrent.md\n//\n// \"If there is significant contention for the writer lock, this mechanism can\n// be inefficient. In this case it is better for the application to use a mutex\n// or some other mechanism that supports blocking to ensure that at most one\n// writer is attempting to COMMIT a BEGIN CONCURRENT transaction at a time.\n// This is usually easier if all writers are part of the same operating system process.\"\nfunc NewSQLiteSingelWriter(db *sql.DB) (*SQLite, error) {\n\tif err := migrate(db, stm); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &SQLite{\n\t\tdb:   db,\n\t\tlock: &sync.Mutex{},\n\t}, nil\n}\n\n// Close the connection\nfunc (s *SQLite) Close() {\n\ts.db.Close()\n}\n\n// Save persists events to the database\nfunc (s *SQLite) Save(events []core.Event) error {\n\t// If no event return no error\n\tif len(events) == 0 {\n\t\treturn nil\n\t}\n\n\tif s.lock != nil {\n\t\t// prevent multiple writers\n\t\ts.lock.Lock()\n\t\tdefer s.lock.Unlock()\n\t}\n\taggregateID := events[0].AggregateID\n\taggregateType := events[0].AggregateType\n\n\ttx, err := s.db.BeginTx(context.Background(), nil)\n\tif err != nil {\n\t\treturn errors.New(fmt.Sprintf(\"could not start a write transaction, %v\", err))\n\t}\n\tdefer tx.Rollback()\n\n\tvar currentVersion core.Version\n\tvar version int\n\tselectStm := `Select version from events where id=? and type=? order by version desc limit 1`\n\terr = tx.QueryRow(selectStm, aggregateID, aggregateType).Scan(&version)\n\tif err != nil && err != sql.ErrNoRows {\n\t\treturn err\n\t} else if err == sql.ErrNoRows {\n\t\t// if no events are saved before set the current version to zero\n\t\tcurrentVersion = core.Version(0)\n\t} else {\n\t\t// set the current version to the last event stored\n\t\tcurrentVersion = core.Version(version)\n\t}\n\n\t// Make sure no other has saved event to the same aggregate concurrently\n\tif core.Version(currentVersion)+1 != events[0].Version {\n\t\treturn core.ErrConcurrency\n\t}\n\n\tvar lastInsertedID int64\n\tinsert := `Insert into events (id, version, reason, type, timestamp, data, metadata) values ($1, $2, $3, $4, $5, $6, $7)`\n\tfor i, event := range events {\n\t\tres, err := tx.Exec(insert, event.AggregateID, event.Version, event.Reason, event.AggregateType, event.Timestamp.Format(time.RFC3339), event.Data, event.Metadata)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlastInsertedID, err = res.LastInsertId()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// override the event in the slice exposing the GlobalVersion to the caller\n\t\tevents[i].GlobalVersion = core.Version(lastInsertedID)\n\t}\n\treturn tx.Commit()\n}\n\n// Get the events from database\nfunc (s *SQLite) Get(ctx context.Context, id string, aggregateType string, afterVersion core.Version) (core.Iterator, error) {\n\tselectStm := `Select seq, id, version, reason, type, timestamp, data, metadata from events where id=? and type=? and version>? order by version asc`\n\trows, err := s.db.QueryContext(ctx, selectStm, id, aggregateType, afterVersion)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Iterator{Rows: rows}, nil\n}\n\n// All iterate over all event in GlobalEvents order\nfunc (s *SQLite) All(start core.Version) core.Fetcher {\n\titer := Iterator{}\n\treturn func() (core.Iterator, error) {\n\t\t// set start from second call and forward\n\t\tif iter.CurrentGlobalVersion != 0 {\n\t\t\tstart = iter.CurrentGlobalVersion + 1\n\t\t}\n\t\tselectStm := `Select seq, id, version, reason, type, timestamp, data, metadata from events where seq >= ? order by seq asc`\n\t\trows, err := s.db.Query(selectStm, start)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\titer.Rows = rows\n\t\treturn &iter, nil\n\t}\n}\n"
  },
  {
    "path": "eventstore/sql/sqlite_test.go",
    "content": "package sql_test\n\nimport (\n\tsqldriver \"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/core/testsuite\"\n\t\"github.com/hallgren/eventsourcing/eventstore/sql\"\n\t_ \"github.com/mattn/go-sqlite3\"\n)\n\nfunc TestSuiteSQLite(t *testing.T) {\n\tf := func() (core.EventStore, func(), error) {\n\t\treturn eventstore(false)\n\t}\n\ttestsuite.Test(t, f)\n}\n\nfunc TestSuiteSQLiteSingelWriter(t *testing.T) {\n\tf := func() (core.EventStore, func(), error) {\n\t\treturn eventstore(true)\n\t}\n\ttestsuite.Test(t, f)\n}\nfunc TestFetchFuncAll(t *testing.T) {\n\tes, close, err := eventstore(false)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer close()\n\ttestsuite.TestFetcher(t, es, es.All(0))\n}\n\nfunc eventstore(singelWriter bool) (*sql.SQLite, func(), error) {\n\tvar es *sql.SQLite\n\tdb, err := sqldriver.Open(\"sqlite3\", \"file::memory:?cache=shared\")\n\tif err != nil {\n\t\treturn nil, nil, errors.New(fmt.Sprintf(\"could not open database %v\", err))\n\t}\n\terr = db.Ping()\n\tif err != nil {\n\t\treturn nil, nil, errors.New(fmt.Sprintf(\"could not ping database %v\", err))\n\t}\n\n\tif singelWriter {\n\t\tes, err = sql.NewSQLiteSingelWriter(db)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t} else {\n\t\t// to make the concurrent test pass (not have to use this in the sql.OpenWithSingelWriter constructor)\n\t\tdb.SetMaxOpenConns(1)\n\t\tes, err = sql.NewSQLite(db)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t}\n\treturn es, func() {\n\t\tes.Close()\n\t}, nil\n}\n"
  },
  {
    "path": "eventstore/sql/sqlserver.go",
    "content": "package sql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\nconst createTableSQLServer = `IF OBJECT_ID('[events]', 'U') IS NULL\nBEGIN\n    CREATE TABLE [events] (\n        [seq] INT IDENTITY(1,1) PRIMARY KEY,\n        [id] NVARCHAR(255) NOT NULL,\n        [version] INT,\n        [reason] NVARCHAR(255),\n        [type] NVARCHAR(255),\n        [timestamp] NVARCHAR(255),\n        [data] VARBINARY(MAX),\n        [metadata] VARBINARY(MAX),\n        CONSTRAINT uq_events UNIQUE ([id], [type], [version])\n    );\nEND`\n\nconst indexSQLServer = `IF NOT EXISTS (\n    SELECT 1 \n    FROM sys.indexes \n    WHERE name = 'id_type' AND object_id = OBJECT_ID('events')\n)\nBEGIN\n    CREATE INDEX id_type ON [events] ([id], [type]);\nEND`\n\nvar stmSQLServer = []string{\n\tcreateTableSQLServer,\n\tindexSQLServer,\n}\n\n// SQLServer event store handler\ntype SQLServer struct {\n\tdb   *sql.DB\n\tlock *sync.Mutex\n}\n\n// NewSQLServer connection to database\nfunc NewSQLServer(db *sql.DB) (*SQLServer, error) {\n\tif err := migrate(db, stmSQLServer); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &SQLServer{\n\t\tdb: db,\n\t}, nil\n}\n\n// Close the connection\nfunc (s *SQLServer) Close() {\n\ts.db.Close()\n}\n\n// Save persists events to the database\nfunc (s *SQLServer) Save(events []core.Event) error {\n\t// If no event return no error\n\tif len(events) == 0 {\n\t\treturn nil\n\t}\n\n\tif s.lock != nil {\n\t\t// prevent multiple writers\n\t\ts.lock.Lock()\n\t\tdefer s.lock.Unlock()\n\t}\n\taggregateID := events[0].AggregateID\n\taggregateType := events[0].AggregateType\n\n\ttx, err := s.db.BeginTx(context.Background(), nil)\n\tif err != nil {\n\t\treturn errors.New(fmt.Sprintf(\"could not start a write transaction, %v\", err))\n\t}\n\tdefer tx.Rollback()\n\n\tvar currentVersion core.Version\n\tvar version int\n\tselectStm := `SELECT TOP 1 version FROM [events] WHERE [id] = @id AND [type] = @type ORDER BY version DESC;`\n\terr = tx.QueryRow(selectStm, sql.Named(\"id\", aggregateID), sql.Named(\"type\", aggregateType)).Scan(&version)\n\tif err != nil && err != sql.ErrNoRows {\n\t\treturn err\n\t} else if err == sql.ErrNoRows {\n\t\t// if no events are saved before set the current version to zero\n\t\tcurrentVersion = core.Version(0)\n\t} else {\n\t\t// set the current version to the last event stored\n\t\tcurrentVersion = core.Version(version)\n\t}\n\n\t// Make sure no other has saved event to the same aggregate concurrently\n\tif core.Version(currentVersion)+1 != events[0].Version {\n\t\treturn core.ErrConcurrency\n\t}\n\n\tvar lastInsertedID int64\n\tinsert := `INSERT INTO [events] (id, version, reason, type, timestamp, data, metadata)\nOUTPUT INSERTED.seq\nVALUES (@id, @version, @reason, @type, @timestamp, @data, @metadata);`\n\tfor i, event := range events {\n\t\terr := tx.QueryRow(\n\t\t\tinsert,\n\t\t\tsql.Named(\"id\", event.AggregateID),\n\t\t\tsql.Named(\"version\", event.Version),\n\t\t\tsql.Named(\"reason\", event.Reason),\n\t\t\tsql.Named(\"type\", event.AggregateType),\n\t\t\tsql.Named(\"timestamp\", event.Timestamp.Format(time.RFC3339)),\n\t\t\tsql.Named(\"data\", event.Data),\n\t\t\tsql.Named(\"metadata\", event.Metadata),\n\t\t).Scan(&lastInsertedID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// override the event in the slice exposing the GlobalVersion to the caller\n\t\tevents[i].GlobalVersion = core.Version(lastInsertedID)\n\t}\n\treturn tx.Commit()\n}\n\n// Get the events from database\nfunc (s *SQLServer) Get(ctx context.Context, id string, aggregateType string, afterVersion core.Version) (core.Iterator, error) {\n\tselectStm := `SELECT seq, id, version, reason, type, timestamp, data, metadata\nFROM [events]\nWHERE id = @id AND type = @type AND version > @version\nORDER BY version ASC;`\n\trows, err := s.db.QueryContext(ctx, selectStm, sql.Named(\"id\", id), sql.Named(\"type\", aggregateType), sql.Named(\"version\", afterVersion))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Iterator{Rows: rows}, nil\n}\n\n// All iterate over all event in GlobalEvents order\nfunc (s *SQLServer) All(start core.Version) core.Fetcher {\n\titer := Iterator{}\n\treturn func() (core.Iterator, error) {\n\t\t// set start from second call and forward\n\t\tif iter.CurrentGlobalVersion != 0 {\n\t\t\tstart = iter.CurrentGlobalVersion + 1\n\t\t}\n\t\tselectStm := `SELECT seq, id, version, reason, type, timestamp, data, metadata\nFROM [events]\nWHERE seq >= @start\nORDER BY seq ASC;`\n\t\trows, err := s.db.Query(selectStm, sql.Named(\"start\", start))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\titer.Rows = rows\n\t\treturn &iter, nil\n\t}\n}\n"
  },
  {
    "path": "eventstore/sql/sqlserver_test.go",
    "content": "package sql_test\n\nimport (\n\t\"context\"\n\tgosql \"database/sql\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t_ \"github.com/denisenkom/go-mssqldb\"\n\t\"github.com/testcontainers/testcontainers-go\"\n\t\"github.com/testcontainers/testcontainers-go/wait\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/core/testsuite\"\n\t\"github.com/hallgren/eventsourcing/eventstore/sql\"\n)\n\nfunc TestSuiteSQLServer(t *testing.T) {\n\tdsn, closer, err := sqlServer()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer closer()\n\n\tf := func() (core.EventStore, func(), error) {\n\t\tes, err := sqlServerConnect(dsn)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\treturn es, es.Close, nil\n\t}\n\ttestsuite.Test(t, f)\n}\n\nfunc TestFetcherAllSQLServer(t *testing.T) {\n\tdsn, closer, err := sqlServer()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer closer()\n\n\tes, err := sqlServerConnect(dsn)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer es.Close()\n\ttestsuite.TestFetcher(t, es, es.All(0))\n}\n\nfunc sqlServerConnect(dsn string) (*sql.SQLServer, error) {\n\tvar db *gosql.DB\n\tvar err error\n\tfor i := 0; i < 10; i++ {\n\t\t// Connect using database/sql\n\t\tdb, err = gosql.Open(\"sqlserver\", dsn)\n\t\t// Test the connection\n\t\tif err == nil && db.Ping() == nil {\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(2 * time.Second)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn sql.NewSQLServer(db)\n}\n\nfunc sqlServer() (string, func(), error) {\n\tctx := context.Background()\n\n\t// Start MSSQL container\n\treq := testcontainers.ContainerRequest{\n\t\tImage:        \"mcr.microsoft.com/mssql/server:2019-latest\",\n\t\tExposedPorts: []string{\"1433/tcp\"},\n\t\tEnv: map[string]string{\n\t\t\t\"ACCEPT_EULA\": \"Y\",\n\t\t\t\"SA_PASSWORD\": \"YourStrong(!)Password\",\n\t\t},\n\t\tWaitingFor: wait.ForLog(\"SQL Server is now ready for client connections\").WithStartupTimeout(2 * time.Minute),\n\t}\n\n\tmssqlC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{\n\t\tContainerRequest: req,\n\t\tStarted:          true,\n\t})\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\thost, err := mssqlC.Host(ctx)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tport, err := mssqlC.MappedPort(ctx, \"1433\")\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tdsn := fmt.Sprintf(\"sqlserver://sa:YourStrong(!)Password@%s:%s?database=master\", host, port.Port())\n\treturn dsn, func() { mssqlC.Terminate(ctx) }, nil\n}\n"
  },
  {
    "path": "example/README.md",
    "content": "## Collection of reference projects\n\n* [Ardan labs](https://github.com/hallgren/ardanlabs) - Example when presenting event sourcing for Ardan Labs.\n* [Internal company presentation](https://github.com/hallgren/kundskapsspridning) - Used in company presentation.\n* [event-sourcing-tech-talk](https://github.com/kieranajp/event-sourcing-tech-talk) - Made by [kieranajp](https://github.com/kieranajp)  "
  },
  {
    "path": "example/go.mod",
    "content": "module github.com/hallgren/eventsourcing/example\n\ngo 1.22\n\nrequire github.com/hallgren/eventsourcing v0.8.1\n\nrequire github.com/hallgren/eventsourcing/core v0.4.0 // indirect\n\n// replace github.com/hallgren/eventsourcing => ../.\n"
  },
  {
    "path": "example/go.sum",
    "content": "github.com/hallgren/eventsourcing v0.8.0 h1:lFMWRdc59+tRwzCzmby8I3qr2WbD1GWqB/ypJrBMbVc=\ngithub.com/hallgren/eventsourcing v0.8.0/go.mod h1:62QwV0m8wLQM6SZwtuGn4lwA5UUFozFnALyZvFeX9ps=\ngithub.com/hallgren/eventsourcing v0.8.1 h1:SHefx3Wf2K1ylmGBzykSmt4dRVnDTQGYjODiM69c4uU=\ngithub.com/hallgren/eventsourcing v0.8.1/go.mod h1:62QwV0m8wLQM6SZwtuGn4lwA5UUFozFnALyZvFeX9ps=\ngithub.com/hallgren/eventsourcing/core v0.4.0 h1:a11TT3df7JlrZtIogqbGmLGgmeugRavwD8HrLtW1Uxw=\ngithub.com/hallgren/eventsourcing/core v0.4.0/go.mod h1:rgo2kFwNVCb0bzUub5nOPlUYNlFkp1uUQBEQx5fM3Lk=\n"
  },
  {
    "path": "example/order/cmd/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/hallgren/eventsourcing\"\n\t\"github.com/hallgren/eventsourcing/aggregate\"\n\t\"github.com/hallgren/eventsourcing/eventstore/memory\"\n\t\"github.com/hallgren/eventsourcing/example/order\"\n)\n\ntype Order struct {\n\tDiscountAmount uint\n\tTotal          uint\n}\n\nfunc main() {\n\tes := memory.Create()\n\taggregate.Register(&order.Order{})\n\n\tongoingOrders := make(map[string]*Order)\n\tcompletedCount := 0\n\tmoneyMade := 0\n\ttotalDiscountAmount := 0\n\n\tgo func() {\n\t\ti := 0\n\t\tfor {\n\t\t\to, err := order.Create(100)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\terr = o.AddDiscount(10)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\t// 33% of orders remove discount\n\t\t\tif i%3 == 0 {\n\t\t\t\to.RemoveDiscount()\n\t\t\t}\n\t\t\terr = o.Pay(80)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\t// 25% of orders are not completed\n\t\t\tif i%4 == 0 {\n\t\t\t\terr = o.Pay(o.Outstanding)\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\t}\n\t\t\terr = aggregate.Save(es, o)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\ttime.Sleep(time.Second)\n\t\t\ti++\n\t\t}\n\t}()\n\n\tfor {\n\t\t// setup how the projection will handle events and build the read model\n\t\tp := eventsourcing.NewProjection(es.All(0, 3), func(e eventsourcing.Event) error {\n\t\t\tswitch event := e.Data().(type) {\n\t\t\t// When an order is created add it to an order map\n\t\t\tcase *order.Created:\n\t\t\t\tongoingOrders[e.AggregateID()] = &Order{\n\t\t\t\t\tTotal: event.Total,\n\t\t\t\t}\n\t\t\t// When order is complete add the discount amount to a sum also store the total amount of completed orders\n\t\t\tcase *order.Completed:\n\t\t\t\tcompletedCount++\n\t\t\t\ttotalDiscountAmount += int(ongoingOrders[e.AggregateID()].DiscountAmount)\n\t\t\t\t// delete the order from the map\n\t\t\t\tdelete(ongoingOrders, e.AggregateID())\n\t\t\t// Store the discount amount in the order in the map\n\t\t\tcase *order.DiscountApplied:\n\t\t\t\tongoingOrders[e.AggregateID()].DiscountAmount = ongoingOrders[e.AggregateID()].Total - event.Total\n\t\t\t// If the discount is removed reset the discount amount in the orders map\n\t\t\tcase *order.DiscountRemoved:\n\t\t\t\tongoingOrders[e.AggregateID()].DiscountAmount = 0\n\t\t\t// If a payment is made store the value\n\t\t\tcase *order.Paid:\n\t\t\t\tmoneyMade += int(event.Amount)\n\t\t\t}\n\t\t\tfmt.Println(\"active order:\", len(ongoingOrders), \"completed orders:\", completedCount, \"money made:\", moneyMade, \"total discount amount:\", totalDiscountAmount)\n\t\t\treturn nil\n\t\t})\n\t\tp.Run(context.Background(), time.Second*2)\n\t}\n}\n"
  },
  {
    "path": "example/order/order.go",
    "content": "package order\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/hallgren/eventsourcing\"\n\t\"github.com/hallgren/eventsourcing/aggregate\"\n)\n\ntype Status string\n\nconst (\n\tPending  Status = \"pending\"\n\tComplete Status = \"complete\"\n)\n\n// Aggregate\n// Collects the business rules for the Order\n//\n// Rules\n// 1. Multiple discounts is not allowed\n// 2. Order amount can't be above 500\n// 3. Completed order can't be altered\n// 4. If a payment has been made it's not possible to alter the discount.\n\n// Order is the aggregate protecting the state\ntype Order struct {\n\taggregate.Root\n\tStatus      Status\n\tTotal       uint\n\tDiscount    uint\n\tOutstanding uint\n\tPaid        uint\n}\n\n// Transition builds the aggregate state based on the events\nfunc (o *Order) Transition(event eventsourcing.Event) {\n\tswitch e := event.Data().(type) {\n\tcase *Created:\n\t\to.Status = Pending\n\t\to.Total = e.Total\n\t\to.Outstanding = e.Total\n\tcase *DiscountApplied:\n\t\to.Discount = e.Percentage\n\t\to.Outstanding = e.Total\n\tcase *DiscountRemoved:\n\t\to.Discount = 0\n\t\to.Outstanding = o.Total\n\tcase *Paid:\n\t\to.Outstanding -= e.Amount\n\t\to.Paid += e.Amount\n\tcase *Completed:\n\t\to.Status = Complete\n\t}\n}\n\n// Events\n// Defines all possible events for the Order aggregate\n\n// Register is a eventsouring helper function that must be defined on\n// the aggregate.\nfunc (o *Order) Register(r aggregate.RegisterFunc) {\n\tr(\n\t\t&Created{},\n\t\t&DiscountApplied{},\n\t\t&DiscountRemoved{},\n\t\t&Paid{},\n\t\t&Completed{},\n\t)\n}\n\n// Created when the order was created\ntype Created struct {\n\tTotal uint\n}\n\n// DiscountApplied when a discount was applied\ntype DiscountApplied struct {\n\tPercentage uint\n\tTotal      uint\n}\n\n// DiscountRemoved when the discount was removed\ntype DiscountRemoved struct{}\n\n// Paid an amount from the total\ntype Paid struct {\n\tAmount uint\n}\n\n// Completed - the order is fully paid\ntype Completed struct{}\n\n// Commands\n// Holds the business logic and protects the aggregate (Order) state.\n// Events should only be created via commands.\n\n// Create creates the initial order\nfunc Create(amount uint) (*Order, error) {\n\tif amount > 500 {\n\t\treturn nil, fmt.Errorf(\"amount can't be higher than 500\")\n\t}\n\n\to := Order{}\n\taggregate.TrackChange(&o, &Created{Total: amount})\n\treturn &o, nil\n}\n\n// AddDiscount adds discount to the order\nfunc (o *Order) AddDiscount(percentage uint) error {\n\tif o.Status == Complete {\n\t\treturn fmt.Errorf(\"can't add discount on completed order\")\n\t}\n\tif o.Discount > 0 {\n\t\treturn fmt.Errorf(\"there is already an active discount\")\n\t}\n\tif o.Paid > 0 {\n\t\treturn fmt.Errorf(\"can't add discount on order with payments\")\n\t}\n\tif percentage > 25 {\n\t\treturn fmt.Errorf(\"discount can't be over 25 was %d\", percentage)\n\t}\n\t// ignore if discount percentage is zero\n\tif percentage == 0 {\n\t\treturn nil\n\t}\n\tdiscountFloat := float64(percentage) / 100.0\n\tnewTotal := o.Total - uint(float64(o.Total)*discountFloat)\n\taggregate.TrackChange(o, &DiscountApplied{Percentage: percentage, Total: newTotal})\n\treturn nil\n}\n\n// RemoveDiscount removes the discount if any otherwise ignore\nfunc (o *Order) RemoveDiscount() {\n\t// No discount applied\n\tif o.Discount == 0 {\n\t\treturn\n\t}\n\taggregate.TrackChange(o, &DiscountRemoved{})\n\treturn\n}\n\n// Pay creates a payment on the order. If the outstanding amount is zero the order\n// is paid.\nfunc (o *Order) Pay(amount uint) error {\n\tif o.Status == Complete {\n\t\treturn fmt.Errorf(\"can't pay on completed order\")\n\t}\n\tif int(o.Outstanding)-int(amount) < 0 {\n\t\treturn fmt.Errorf(\"payment is higher than order total amount\")\n\t}\n\n\taggregate.TrackChange(o, &Paid{Amount: amount})\n\n\tif o.Outstanding == 0 {\n\t\taggregate.TrackChange(o, &Completed{})\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "example/order/order_test.go",
    "content": "package order_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/example/order\"\n)\n\nfunc TestCreateOrder(t *testing.T) {\n\to, err := order.Create(1000)\n\tif err == nil {\n\t\tt.Fatal(\"expected error due to for high amount\")\n\t}\n\n\to, err = order.Create(100)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif o.Status != order.Pending {\n\t\tt.Fatalf(\"expected order status to be pending but was %s\", o.Status)\n\t}\n\tif o.Total != 100 {\n\t\tt.Fatalf(\"expected order total to be 100 but was %d\", o.Total)\n\t}\n\tif o.Outstanding != 100 {\n\t\tt.Fatalf(\"expected order outstanding to be 100 but was %d\", o.Outstanding)\n\t}\n}\n\nfunc TestDiscount(t *testing.T) {\n\to, err := order.Create(100)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\terr = o.AddDiscount(40)\n\tif err == nil {\n\t\tt.Fatal(err)\n\t}\n\terr = o.AddDiscount(20)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\t// not possible to add multiple discounts\n\terr = o.AddDiscount(4)\n\tif err == nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif o.Outstanding == o.Total {\n\t\tt.Fatalf(\"expected outstanding (%d) to be less with a discount but was same as Total (%d)\", o.Outstanding, o.Total)\n\t}\n\n\to.RemoveDiscount()\n\tif o.Outstanding != o.Total {\n\t\tt.Fatalf(\"expected order total (%d) to be same as outstandingi (%d)\", o.Outstanding, o.Total)\n\t}\n\n\tif o.Status != order.Pending {\n\t\tt.Fatalf(\"order status should be pending but was %s\", o.Status)\n\t}\n}\n\nfunc TestPaid(t *testing.T) {\n\to, err := order.Create(100)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = o.Pay(200)\n\tif err == nil {\n\t\tt.Fatal(\"should not be abel to pay more than total amount\")\n\t}\n\n\terr = o.Pay(10)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif o.Outstanding != 100-10 {\n\t\tt.Fatalf(\"expected order outstanding to be 90 but was %d\", o.Outstanding)\n\t}\n\n\terr = o.Pay(90)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif o.Outstanding != 0 {\n\t\tt.Fatalf(\"expected outstanding to be zero but was %d\", o.Outstanding)\n\t}\n\n\tif o.Status != order.Complete {\n\t\tt.Fatalf(\"expexted status to be complete but was %s\", o.Status)\n\t}\n\n\terr = o.Pay(10)\n\tif err == nil {\n\t\tt.Fatal(\"should not be able to pay on complated order\")\n\t}\n}\n"
  },
  {
    "path": "example/tictactoe/cmd/main/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\n\t\"github.com/hallgren/eventsourcing/aggregate\"\n\t\"github.com/hallgren/eventsourcing/eventstore/memory\"\n\t\"github.com/hallgren/eventsourcing/example/tictactoe\"\n)\n\nfunc main() {\n\tes := memory.Create()\n\taggregate.Register(&tictactoe.Game{})\n\tfor i := 0; i < 10; i++ {\n\t\tgame := PlayGame()\n\t\tfmt.Printf(\"game %d\\n\", i)\n\t\tgame.Render()\n\t\taggregate.Save(es, game)\n\t}\n}\n\nfunc PlayGame() *tictactoe.Game {\n\tgame := tictactoe.NewGame()\n\tfor !game.Done() {\n\t\tx := rand.Intn(3)\n\t\ty := rand.Intn(3)\n\t\tgame.PlayMove(x, y)\n\t}\n\treturn game\n}\n"
  },
  {
    "path": "example/tictactoe/tictactoe.go",
    "content": "package tictactoe\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/hallgren/eventsourcing\"\n\t\"github.com/hallgren/eventsourcing/aggregate\"\n)\n\ntype Game struct {\n\taggregate.Root\n\tboard    [3][3]string // \"X\", \"O\", or \"\"\n\tturn     string       // \"X\" or \"O\"\n\tgameOver bool\n\twinner   string // \"X\", \"O\", or \"\"\n}\n\n// Transition is an method to transform events to game state. The state is then used\n// to uphold the rules of the game. This method is needed for the Game to be an aggregate\n// and be used by the functions in the aggregate package.\nfunc (g *Game) Transition(event eventsourcing.Event) {\n\tswitch e := event.Data().(type) {\n\tcase *Started:\n\t\tg.turn = \"X\"\n\tcase *XMoved:\n\t\tg.board[e.X][e.Y] = \"X\"\n\t\tg.turn = \"O\"\n\tcase *OMoved:\n\t\tg.board[e.X][e.Y] = \"O\"\n\t\tg.turn = \"X\"\n\tcase *Draw:\n\t\tg.gameOver = true\n\t\tg.winner = \"\"\n\tcase *XWon:\n\t\tg.gameOver = true\n\t\tg.winner = \"X\"\n\tcase *OWon:\n\t\tg.gameOver = true\n\t\tg.winner = \"O\"\n\t}\n}\n\n// Register is used to register the events and the aggregate to the internal register.\nfunc (g *Game) Register(f aggregate.RegisterFunc) {\n\tf(&Started{}, &XMoved{}, &OMoved{}, &Draw{}, &XWon{}, &OWon{})\n}\n\n// Events\n\n// Started indicates that the game has started\ntype Started struct{}\n\n// XMoved then the X player moved togheter with the cordinates.\ntype XMoved struct {\n\tX int\n\tY int\n}\n\n// OMoved then the O player moved togheter with the cordinates.\ntype OMoved struct {\n\tX int\n\tY int\n}\n\n// XWon is the last event when X is the winner\ntype XWon struct{}\n\n// OWon is the last event when O is the winner\ntype OWon struct{}\n\n// Draw is when either X now O won\ntype Draw struct{}\n\n// Constructor\nfunc NewGame() *Game {\n\tg := Game{}\n\taggregate.TrackChange(&g, &Started{})\n\treturn &g\n}\n\n// Query methods\nfunc (g *Game) Turn() string {\n\treturn g.turn\n}\n\nfunc (g *Game) Done() bool {\n\treturn g.gameOver\n}\n\nfunc (g *Game) Winner() string {\n\treturn g.winner\n}\n\n// Render prints the board\nfunc (g *Game) Render() {\n\tfor i, row := range g.board {\n\t\tfor j, cell := range row {\n\t\t\tif cell == \"\" {\n\t\t\t\tfmt.Print(\"   \")\n\t\t\t} else {\n\t\t\t\tfmt.Printf(\" %s \", cell)\n\t\t\t}\n\t\t\tif j < len(row)-1 {\n\t\t\t\tfmt.Print(\"|\")\n\t\t\t}\n\t\t}\n\t\tfmt.Println()\n\t\tif i < len(g.board)-1 {\n\t\t\tfmt.Println(\"---+---+---\")\n\t\t}\n\t}\n}\n\n// Commands\nfunc (g *Game) PlayMove(x, y int) error {\n\t// verify that the move can be made\n\tif g.gameOver {\n\t\treturn fmt.Errorf(\"game over\")\n\t}\n\tif g.board[x][y] != \"\" {\n\t\treturn fmt.Errorf(\"position already taken\")\n\t}\n\n\tif g.turn == \"X\" {\n\t\taggregate.TrackChange(g, &XMoved{x, y})\n\t} else {\n\t\taggregate.TrackChange(g, &OMoved{x, y})\n\t}\n\n\twinner := checkWinner(g.board)\n\tif winner == \"X\" {\n\t\taggregate.TrackChange(g, &XWon{})\n\t\treturn nil\n\t}\n\tif winner == \"O\" {\n\t\taggregate.TrackChange(g, &OWon{})\n\t\treturn nil\n\t}\n\n\tif isDraw(g.board) {\n\t\taggregate.TrackChange(g, &Draw{})\n\t}\n\n\treturn nil\n}\n\n// helpers\n// checkWinner returns \"X\" or \"O\" if there's a winner, or \"\" if none.\nfunc checkWinner(board [3][3]string) string {\n\t// Check rows and columns\n\tfor i := 0; i < 3; i++ {\n\t\tif board[i][0] != \"\" && board[i][0] == board[i][1] && board[i][1] == board[i][2] {\n\t\t\treturn board[i][0] // row win\n\t\t}\n\t\tif board[0][i] != \"\" && board[0][i] == board[1][i] && board[1][i] == board[2][i] {\n\t\t\treturn board[0][i] // column win\n\t\t}\n\t}\n\n\t// Check diagonals\n\tif board[0][0] != \"\" && board[0][0] == board[1][1] && board[1][1] == board[2][2] {\n\t\treturn board[0][0]\n\t}\n\tif board[0][2] != \"\" && board[0][2] == board[1][1] && board[1][1] == board[2][0] {\n\t\treturn board[0][2]\n\t}\n\n\t// No winner\n\treturn \"\"\n}\n\nfunc isDraw(board [3][3]string) bool {\n\tif checkWinner(board) != \"\" {\n\t\treturn false\n\t}\n\tfor _, row := range board {\n\t\tfor _, cell := range row {\n\t\t\tif cell == \"\" {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "example/tictactoe/tictactoe_test.go",
    "content": "package tictactoe_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/example/tictactoe\"\n)\n\nfunc TestValidMove(t *testing.T) {\n\tgame := tictactoe.NewGame()\n\terr := game.PlayMove(0, 0)\n\tif err != nil {\n\t\tt.Errorf(\"Expected valid move, got error: %v\", err)\n\t}\n\t// verify the events\n\tif len(game.Events()) != 2 {\n\t\tt.Fatalf(\"expected two events got %d\", len(game.Events()))\n\t}\n\n\tif game.Events()[0].Reason() != \"Started\" {\n\t\tt.Fatalf(\"expected first event to be started was %v\", game.Events()[0].Reason())\n\t}\n\n\tswitch e := game.Events()[1].Data().(type) {\n\tcase *tictactoe.XMoved:\n\t\tif e.X != 0 && e.Y != 0 {\n\t\t\tt.Fatalf(\"Expected 'X' at 0,0, got %d,%d\", e.X, e.Y)\n\t\t}\n\tdefault:\n\t\tt.Fatal(\"expeted XMoved event\")\n\t}\n}\n\nfunc TestInvalidMoveAlreadyTaken(t *testing.T) {\n\tgame := tictactoe.NewGame()\n\t_ = game.PlayMove(0, 0)\n\terr := game.PlayMove(0, 0)\n\tif err == nil {\n\t\tt.Errorf(\"Expected error for move on occupied square, got nil\")\n\t}\n}\n\nfunc TestTurnSwitching(t *testing.T) {\n\tgame := tictactoe.NewGame()\n\t_ = game.PlayMove(0, 0)\n\tif game.Turn() != \"O\" {\n\t\tt.Errorf(\"Expected turn to switch to O, got %s\", game.Turn())\n\t}\n}\n\nfunc TestWinDetection(t *testing.T) {\n\tgame := tictactoe.NewGame()\n\tgame.PlayMove(0, 0)\n\tgame.PlayMove(1, 0)\n\tgame.PlayMove(0, 1)\n\tgame.PlayMove(1, 1)\n\tgame.PlayMove(0, 2) // X wins\n\tif !game.Done() || game.Winner() != \"X\" {\n\t\tt.Errorf(\"Expected X to win, got GameOver=%v, Winner=%s\", game.Done(), game.Winner())\n\t}\n\n\t// make sure the last event is XWon\n\tl := len(game.Events())\n\tswitch game.Events()[l-1].Data().(type) {\n\tcase *tictactoe.XWon:\n\tdefault:\n\t\tt.Fatalf(\"expected last event to be XWon but was %v\", game.Events()[l-1].Reason())\n\t}\n}\n\nfunc TestDrawDetection(t *testing.T) {\n\tgame := tictactoe.NewGame()\n\t// first row\n\tgame.PlayMove(0, 0) // X\n\tgame.PlayMove(0, 1) // O\n\tgame.PlayMove(0, 2) // X\n\t// second row\n\tgame.PlayMove(1, 1) // O\n\tgame.PlayMove(1, 0) // X\n\tgame.PlayMove(1, 2) // O\n\t// third row\n\tgame.PlayMove(2, 1)     // X\n\tgame.PlayMove(2, 0)     // O\n\t_ = game.PlayMove(2, 2) // X\n\tif !game.Done() || game.Winner() != \"\" {\n\t\tt.Errorf(\"Expected draw, got GameOver=%v, Winner=%s\", game.Done(), game.Winner())\n\t}\n\t// make sure the last event is Draw\n\tl := len(game.Events())\n\tswitch game.Events()[l-1].Data().(type) {\n\tcase *tictactoe.Draw:\n\tdefault:\n\t\tt.Fatalf(\"expected last event to be Draw but was %v\", game.Events()[l-1].Reason())\n\t}\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/hallgren/eventsourcing\n\ngo 1.19\n\nrequire github.com/hallgren/eventsourcing/core v0.5.2\n\n// replace github.com/hallgren/eventsourcing/core => ./core\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/hallgren/eventsourcing/core v0.5.2 h1:knvM1jP0zziiybce+Au7ysYvZQnDwxkV+/RFZWNDMiw=\ngithub.com/hallgren/eventsourcing/core v0.5.2/go.mod h1:rgo2kFwNVCb0bzUub5nOPlUYNlFkp1uUQBEQx5fM3Lk=\n"
  },
  {
    "path": "internal/encoderjson.go",
    "content": "package internal\n\nimport \"encoding/json\"\n\ntype EncoderJSON struct{}\n\nfunc (e EncoderJSON) Serialize(v interface{}) ([]byte, error) {\n\treturn json.Marshal(v)\n}\n\nfunc (e EncoderJSON) Deserialize(data []byte, v interface{}) error {\n\treturn json.Unmarshal(data, v)\n}\n\ntype encoder interface {\n\tSerialize(v interface{}) ([]byte, error)\n\tDeserialize(data []byte, v interface{}) error\n}\n\n// global encoder used for events\nvar EventEncoder encoder = EncoderJSON{}\n\n// global encoder used for snapshots\nvar SnapshotEncoder encoder = EncoderJSON{}\n"
  },
  {
    "path": "internal/register.go",
    "content": "package internal\n\nimport (\n\t\"reflect\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\ntype registerFunc = func() interface{}\n\ntype register struct {\n\teventsF    map[string]registerFunc\n\taggregates map[string]struct{}\n}\n\n// Aggregate interface to use the aggregate root specific methods\ntype aggregate interface {\n\tRegister(func(events ...interface{}))\n}\n\n// GlobalRegister keeps track of registered aggregates and events\nvar GlobalRegister = newRegister()\n\n// ResetRegister reset the event regsiter\nfunc ResetRegister() {\n\tGlobalRegister = newRegister()\n}\n\nfunc newRegister() *register {\n\treturn &register{\n\t\teventsF:    make(map[string]registerFunc),\n\t\taggregates: make(map[string]struct{}),\n\t}\n}\n\n// aggregateRegistered return true if the aggregate is registered\nfunc (r *register) AggregateRegistered(a aggregate) bool {\n\ttyp := aggregateType(a)\n\t_, ok := r.aggregates[typ]\n\treturn ok\n}\n\n// EventRegistered return the func to generate the correct event data type and true if it exists\n// otherwise false.\nfunc (r *register) EventRegistered(event core.Event) (registerFunc, bool) {\n\td, ok := r.eventsF[event.AggregateType+\"_\"+event.Reason]\n\treturn d, ok\n}\n\n// Register store the aggregate and calls the aggregate method Register to Register the aggregate events.\nfunc (r *register) Register(a aggregate) {\n\ttyp := reflect.TypeOf(a).Elem().Name()\n\tfu := r.RegisterAggregate(typ)\n\ta.Register(fu)\n}\n\nfunc (r *register) RegisterAggregate(aggregateType string) func(events ...interface{}) {\n\tr.aggregates[aggregateType] = struct{}{}\n\n\t// fe is a helper function to make the event type registration simpler\n\tfe := func(events ...interface{}) []registerFunc {\n\t\tres := []registerFunc{}\n\t\tfor _, e := range events {\n\t\t\tres = append(res, eventToFunc(e))\n\t\t}\n\t\treturn res\n\t}\n\n\treturn func(events ...interface{}) {\n\t\teventsF := fe(events...)\n\t\tfor _, f := range eventsF {\n\t\t\tevent := f()\n\t\t\treason := reflect.TypeOf(event).Elem().Name()\n\t\t\tr.eventsF[aggregateType+\"_\"+reason] = f\n\t\t}\n\t}\n}\n\nfunc eventToFunc(event interface{}) registerFunc {\n\treturn func() interface{} {\n\t\t// return a new instance of the event\n\t\treturn reflect.New(reflect.TypeOf(event).Elem()).Interface()\n\t}\n}\n\nfunc aggregateType(a aggregate) string {\n\treturn reflect.TypeOf(a).Elem().Name()\n}\n"
  },
  {
    "path": "iterator.go",
    "content": "package eventsourcing\n\nimport (\n\t\"fmt\"\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/internal\"\n)\n\n// Iterator to stream events to reduce memory foot print\ntype Iterator struct {\n\tCoreIterator core.Iterator\n}\n\n// Close the underlaying iterator\nfunc (i *Iterator) Close() {\n\ti.CoreIterator.Close()\n}\n\nfunc (i *Iterator) Next() bool {\n\treturn i.CoreIterator.Next()\n}\n\nfunc (i *Iterator) Value() (Event, error) {\n\tevent, err := i.CoreIterator.Value()\n\tif err != nil {\n\t\treturn Event{}, err\n\t}\n\t// apply the event to the aggregate\n\tf, found := internal.GlobalRegister.EventRegistered(event)\n\tif !found {\n\t\treturn Event{}, fmt.Errorf(\"event not registered, aggregate type: %s, reason: %s, global version: %d, %w\", event.AggregateType, event.Reason, event.GlobalVersion, ErrEventNotRegistered)\n\t}\n\tdata := f()\n\terr = internal.EventEncoder.Deserialize(event.Data, &data)\n\tif err != nil {\n\t\treturn Event{}, err\n\t}\n\tmetadata := make(map[string]interface{})\n\tif event.Metadata != nil {\n\t\terr = internal.EventEncoder.Deserialize(event.Metadata, &metadata)\n\t\tif err != nil {\n\t\t\treturn Event{}, err\n\t\t}\n\t}\n\treturn Event{\n\t\tevent:    event,\n\t\tdata:     data,\n\t\tmetadata: metadata,\n\t}, nil\n}\n"
  },
  {
    "path": "projections.go",
    "content": "package eventsourcing\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\ntype callbackFunc func(e Event) error\n\n// ErrProjectionAlreadyRunning is returned if Run is called on an already running projection\nvar ErrProjectionAlreadyRunning = errors.New(\"projection is already running\")\n\ntype Projection struct {\n\trunning   atomic.Bool\n\tfetchF    core.Fetcher\n\tcallbackF callbackFunc\n\ttrigger   chan func()\n\tStrict    bool // Strict indicate if the projection should return error if the event it fetches is not found in the register\n\tName      string\n}\n\n// ProjectionGroup runs projections concurrently\ntype ProjectionGroup struct {\n\tPace        time.Duration // Pace is used when a projection is running and it reaches the end of the event stream\n\tprojections []*Projection\n\tcancelF     context.CancelFunc\n\twg          sync.WaitGroup\n\tErrChan     chan error\n}\n\n// ProjectionResult is the return type for a Group and Race\ntype ProjectionResult struct {\n\tError            error\n\tName             string\n\tLastHandledEvent Event\n}\n\n// Projection creates a projection that will run down an event stream\nfunc NewProjection(fetchF core.Fetcher, callbackF callbackFunc) *Projection {\n\tprojection := Projection{\n\t\tfetchF:    fetchF,\n\t\tcallbackF: callbackF,\n\t\ttrigger:   make(chan func()),\n\t\tStrict:    true, // Default strict is active\n\t}\n\treturn &projection\n}\n\n// TriggerAsync force a running projection to run immediately independent on the pace\n// It will return immediately after triggering the prjection to run.\n// If the trigger channel is already filled it will return without inserting any value.\nfunc (p *Projection) TriggerAsync() {\n\tif !p.running.Load() {\n\t\treturn\n\t}\n\tselect {\n\tcase p.trigger <- func() {}:\n\tdefault:\n\t}\n}\n\n// TriggerSync force a running projection to run immediately independent on the pace\n// It will wait for the projection to finish running to its current end before returning.\nfunc (p *Projection) TriggerSync() {\n\tif !p.running.Load() {\n\t\treturn\n\t}\n\twg := sync.WaitGroup{}\n\twg.Add(1)\n\tf := func() {\n\t\twg.Done()\n\t}\n\tp.trigger <- f\n\twg.Wait()\n}\n\n// Run runs the projection forever until the context is cancelled. When there are no more events to consume it\n// waits for a trigger or context cancel.\nfunc (p *Projection) Run(ctx context.Context, pace time.Duration) error {\n\tif p.running.Load() {\n\t\treturn ErrProjectionAlreadyRunning\n\t}\n\tp.running.Store(true)\n\tdefer func() {\n\t\tp.running.Store(false)\n\t}()\n\n\tvar noopFunc = func() {}\n\tvar f = noopFunc\n\ttriggerFunc := func() {\n\t\tf()\n\t\t// reset the f to the noop func\n\t\tf = noopFunc\n\t}\n\tfor {\n\t\tresult := p.RunToEnd(ctx)\n\t\t// if triggered by a sync trigger the triggerFunc callback that it's finished\n\t\t// if not triggered by a sync trigger the triggerFunc will call an no ops function\n\t\ttriggerFunc()\n\t\tif result.Error != nil {\n\t\t\treturn result.Error\n\t\t}\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tcase <-time.After(pace):\n\t\tcase f = <-p.trigger:\n\t\t}\n\t}\n}\n\n// RunToEnd runs until the projection reaches the end of the event stream\nfunc (p *Projection) RunToEnd(ctx context.Context) ProjectionResult {\n\tvar result ProjectionResult\n\tvar lastHandledEvent Event\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ProjectionResult{Error: ctx.Err(), Name: result.Name, LastHandledEvent: result.LastHandledEvent}\n\t\tdefault:\n\t\t\tran, result := p.RunOnce()\n\t\t\t// if the first event returned error or if it did not run at all\n\t\t\tif result.LastHandledEvent.GlobalVersion() == 0 {\n\t\t\t\tresult.LastHandledEvent = lastHandledEvent\n\t\t\t}\n\t\t\tif result.Error != nil {\n\t\t\t\treturn result\n\t\t\t}\n\t\t\t// hit the end of the event stream\n\t\t\tif !ran {\n\t\t\t\treturn result\n\t\t\t}\n\t\t\tlastHandledEvent = result.LastHandledEvent\n\t\t}\n\t}\n}\n\n// RunOnce runs the fetch method one time\nfunc (p *Projection) RunOnce() (bool, ProjectionResult) {\n\t// ran indicate if there were events to fetch\n\tvar ran bool\n\tvar lastHandledEvent Event\n\n\tcoreIterator, err := p.fetchF()\n\tif err != nil {\n\t\treturn false, ProjectionResult{Error: err, Name: p.Name, LastHandledEvent: lastHandledEvent}\n\t}\n\titerator := &Iterator{\n\t\tCoreIterator: coreIterator,\n\t}\n\tdefer iterator.Close()\n\n\tfor iterator.Next() {\n\t\tran = true\n\t\tevent, err := iterator.Value()\n\t\tif err != nil {\n\t\t\tif errors.Is(err, ErrEventNotRegistered) {\n\t\t\t\tif p.Strict {\n\t\t\t\t\treturn false, ProjectionResult{Error: err, Name: p.Name, LastHandledEvent: lastHandledEvent}\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn false, ProjectionResult{Error: err, Name: p.Name, LastHandledEvent: lastHandledEvent}\n\t\t}\n\n\t\terr = p.callbackF(event)\n\t\tif err != nil {\n\t\t\treturn false, ProjectionResult{Error: err, Name: p.Name, LastHandledEvent: lastHandledEvent}\n\t\t}\n\t\t// keep a reference to the last successfully handled event\n\t\tlastHandledEvent = event\n\t}\n\treturn ran, ProjectionResult{Error: nil, Name: p.Name, LastHandledEvent: lastHandledEvent}\n}\n\n// Group runs a group of projections concurrently\nfunc NewProjectionGroup(projections ...*Projection) *ProjectionGroup {\n\treturn &ProjectionGroup{\n\t\tprojections: projections,\n\t\tcancelF:     func() {},\n\t\tPace:        time.Second * 10, // Default pace 10 seconds\n\t}\n}\n\n// Start starts all projectinos in the group, an error channel i created on the group to notify\n// if a result containing an error is returned from a projection\nfunc (g *ProjectionGroup) Start() {\n\tg.ErrChan = make(chan error)\n\tctx, cancel := context.WithCancel(context.Background())\n\tg.cancelF = cancel\n\n\tg.wg.Add(len(g.projections))\n\tfor _, projection := range g.projections {\n\t\tgo func(p *Projection) {\n\t\t\tdefer g.wg.Done()\n\t\t\terr := p.Run(ctx, g.Pace)\n\t\t\tif !errors.Is(err, context.Canceled) {\n\t\t\t\tg.ErrChan <- err\n\t\t\t}\n\t\t}(projection)\n\t}\n}\n\n// TriggerAsync force all projections to run not waiting for them to finish\nfunc (g *ProjectionGroup) TriggerAsync() {\n\tfor _, projection := range g.projections {\n\t\tprojection.TriggerAsync()\n\t}\n}\n\n// TriggerSync force all projections to run and wait for them to finish\nfunc (g *ProjectionGroup) TriggerSync() {\n\twg := sync.WaitGroup{}\n\tfor _, projection := range g.projections {\n\t\twg.Add(1)\n\t\tgo func(p *Projection) {\n\t\t\tp.TriggerSync()\n\t\t\twg.Done()\n\t\t}(projection)\n\t}\n\twg.Wait()\n}\n\n// Stop halts all projections in the group\nfunc (g *ProjectionGroup) Stop() {\n\tif g.ErrChan == nil {\n\t\treturn\n\t}\n\tg.cancelF()\n\n\t// return when all projections has stopped\n\tg.wg.Wait()\n\n\t// close the error channel\n\tclose(g.ErrChan)\n\n\tg.ErrChan = nil\n}\n\n// ProjectionsRace runs the projections to the end of the events streams.\n// Can be used on a stale event stream with no more events coming in or when you want to know when all projections are done.\nfunc ProjectionsRace(cancelOnError bool, projections ...*Projection) ([]ProjectionResult, error) {\n\tvar lock sync.Mutex\n\tvar causingErr error\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\twg := sync.WaitGroup{}\n\twg.Add(len(projections))\n\n\tresults := make([]ProjectionResult, len(projections))\n\tfor i, projection := range projections {\n\t\tgo func(pr *Projection, index int) {\n\t\t\tdefer wg.Done()\n\t\t\tresult := pr.RunToEnd(ctx)\n\t\t\tif result.Error != nil {\n\t\t\t\tif !errors.Is(result.Error, context.Canceled) && cancelOnError {\n\t\t\t\t\tcancel()\n\n\t\t\t\t\tlock.Lock()\n\t\t\t\t\tcausingErr = result.Error\n\t\t\t\t\tlock.Unlock()\n\t\t\t\t}\n\t\t\t}\n\t\t\tlock.Lock()\n\t\t\tresults[index] = result\n\t\t\tlock.Unlock()\n\t\t}(projection, i)\n\t}\n\twg.Wait()\n\treturn results, causingErr\n}\n"
  },
  {
    "path": "projections_test.go",
    "content": "package eventsourcing_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/hallgren/eventsourcing\"\n\t\"github.com/hallgren/eventsourcing/aggregate\"\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/eventstore/memory\"\n\t\"github.com/hallgren/eventsourcing/internal\"\n)\n\n// Person aggregate\ntype Person struct {\n\taggregate.Root\n\tName string\n\tAge  int\n\tDead int\n}\n\n// Born event\ntype Born struct {\n\tName string\n}\n\n// AgedOneYear event\ntype AgedOneYear struct {\n}\n\n// CreatePerson constructor for the Person\nfunc CreatePerson(name string) (*Person, error) {\n\tif name == \"\" {\n\t\treturn nil, errors.New(\"name can't be blank\")\n\t}\n\tperson := Person{}\n\taggregate.TrackChange(&person, &Born{Name: name})\n\treturn &person, nil\n}\n\n// Transition the person state dependent on the events\nfunc (person *Person) Transition(event eventsourcing.Event) {\n\tswitch e := event.Data().(type) {\n\tcase *Born:\n\t\tperson.Age = 0\n\t\tperson.Name = e.Name\n\tcase *AgedOneYear:\n\t\tperson.Age += 1\n\t}\n}\n\n// Register bind the events to the repository when the aggregate is registered.\nfunc (person *Person) Register(f aggregate.RegisterFunc) {\n\tf(&Born{}, &AgedOneYear{})\n}\n\n// GrowOlder command\nfunc (person *Person) GrowOlder() {\n\tmetaData := make(map[string]interface{})\n\tmetaData[\"foo\"] = \"bar\"\n\taggregate.TrackChangeWithMetadata(person, &AgedOneYear{}, metaData)\n}\n\nfunc createPersonEvent(es *memory.Memory, name string, age int) error {\n\tperson, err := CreatePerson(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor i := 0; i < age; i++ {\n\t\tperson.GrowOlder()\n\t}\n\n\tevents := make([]core.Event, 0)\n\tfor _, e := range person.Events() {\n\t\tdata, err := json.Marshal(e.Data())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tevents = append(events, core.Event{\n\t\t\tAggregateID:   e.AggregateID(),\n\t\t\tReason:        e.Reason(),\n\t\t\tAggregateType: e.AggregateType(),\n\t\t\tVersion:       core.Version(e.Version()),\n\t\t\tGlobalVersion: core.Version(e.GlobalVersion()),\n\t\t\tTimestamp:     e.Timestamp(),\n\t\t\tData:          data,\n\t\t})\n\t}\n\treturn es.Save(events)\n}\n\nfunc TestRunOnce(t *testing.T) {\n\t// setup\n\tes := memory.Create()\n\taggregate.Register(&Person{})\n\n\tprojectedName := \"\"\n\n\terr := createPersonEvent(es, \"kalle\", 0)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = createPersonEvent(es, \"anka\", 0)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// run projection one event at each run\n\tproj := eventsourcing.NewProjection(es.All(0, 1), func(event eventsourcing.Event) error {\n\t\tswitch e := event.Data().(type) {\n\t\tcase *Born:\n\t\t\tprojectedName = e.Name\n\t\t}\n\t\treturn nil\n\t})\n\n\t// should set projectedName to kalle\n\twork, result := proj.RunOnce()\n\tif result.Error != nil {\n\t\tt.Fatal(result)\n\t}\n\n\tif !work {\n\t\tt.Fatal(\"there was no work to do\")\n\t}\n\tif projectedName != \"kalle\" {\n\t\tt.Fatalf(\"expected %q was %q\", \"kalle\", projectedName)\n\t}\n\n\t// should set the projected name to anka\n\twork, result = proj.RunOnce()\n\tif result.Error != nil {\n\t\tt.Fatal(result.Error)\n\t}\n\n\tif !work {\n\t\tt.Fatal(\"there was no work to do\")\n\t}\n\tif projectedName != \"anka\" {\n\t\tt.Fatalf(\"expected %q was %q\", \"anka\", projectedName)\n\t}\n}\n\nfunc TestRun(t *testing.T) {\n\tes := memory.Create()\n\taggregate.Register(&Person{})\n\n\tprojectedName := \"\"\n\tsourceName := \"kalle\"\n\n\terr := createPersonEvent(es, sourceName, 1)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// run projection\n\tproj := eventsourcing.NewProjection(es.All(0, 1), func(event eventsourcing.Event) error {\n\t\tswitch e := event.Data().(type) {\n\t\tcase *Born:\n\t\t\tprojectedName = e.Name\n\t\t}\n\t\treturn nil\n\t})\n\n\tctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second))\n\tdefer cancel()\n\n\t// will run once then sleep for 10 seconds\n\terr = proj.Run(ctx, time.Second*10)\n\tif !errors.Is(err, context.DeadlineExceeded) {\n\t\tt.Fatal(err)\n\t}\n\n\tif projectedName != sourceName {\n\t\tt.Fatalf(\"expected %q was %q\", sourceName, projectedName)\n\t}\n}\n\nfunc TestRunSameProjectionConcurrently(t *testing.T) {\n\t// setup\n\tes := memory.Create()\n\taggregate.Register(&Person{})\n\n\tsourceName := \"kalle\"\n\n\terr := createPersonEvent(es, sourceName, 0)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\twg := sync.WaitGroup{}\n\twg.Add(1)\n\t// run projection\n\tproj := eventsourcing.NewProjection(es.All(0, 1), func(event eventsourcing.Event) error {\n\t\twg.Done()\n\t\treturn nil\n\t})\n\n\tctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second))\n\tdefer cancel()\n\n\t// Run the projection\n\tgo func() {\n\t\tproj.Run(ctx, time.Second*10)\n\t}()\n\n\t// wait to make sure the projection is already running\n\twg.Wait()\n\n\terr = proj.Run(ctx, time.Second*10)\n\tif !errors.Is(err, eventsourcing.ErrProjectionAlreadyRunning) {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestTriggerSync(t *testing.T) {\n\t// setup\n\tes := memory.Create()\n\taggregate.Register(&Person{})\n\n\tprojectedName := \"\"\n\tsourceName := \"kalle\"\n\n\t// run projection\n\tproj := eventsourcing.NewProjection(es.All(0, 1), func(event eventsourcing.Event) error {\n\t\tswitch e := event.Data().(type) {\n\t\tcase *Born:\n\t\t\tprojectedName = e.Name\n\t\t}\n\t\treturn nil\n\t})\n\n\tgroup := eventsourcing.NewProjectionGroup(proj)\n\tgroup.Start()\n\tdefer group.Stop()\n\n\t// make sure the projection has finished it's first round\n\ttime.Sleep(time.Millisecond * 10)\n\n\t// create the event after the projection is started as the projection would have consume it.\n\terr := createPersonEvent(es, sourceName, 1)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// check projection is not updated before trigger\n\tif projectedName == sourceName {\n\t\tt.Fatalf(\"expected projected name to differ: %q was %q\", sourceName, projectedName)\n\t}\n\n\t// trigger the projection\n\tgroup.TriggerSync()\n\n\t// check that the projected value is updated\n\tif projectedName != sourceName {\n\t\tt.Fatalf(\"expected projected name: %q was %q\", sourceName, projectedName)\n\t}\n}\n\nfunc TestTriggerAsync(t *testing.T) {\n\t// setup\n\tes := memory.Create()\n\taggregate.Register(&Person{})\n\n\tprojectedName := \"\"\n\tsourceName := \"kalle\"\n\n\twg := sync.WaitGroup{}\n\twg.Add(1)\n\n\t// run projection\n\tproj := eventsourcing.NewProjection(es.All(0, 1), func(event eventsourcing.Event) error {\n\t\tswitch e := event.Data().(type) {\n\t\tcase *Born:\n\t\t\tprojectedName = e.Name\n\t\t}\n\t\twg.Done()\n\t\treturn nil\n\t})\n\n\tgroup := eventsourcing.NewProjectionGroup(proj)\n\tgroup.Start()\n\tdefer group.Stop()\n\n\t// make sure the projection has finished it's first round\n\ttime.Sleep(time.Millisecond * 10)\n\n\t// create the event after the projection is started as the projection would have consume it.\n\terr := createPersonEvent(es, sourceName, 0)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// check projection is not updated before trigger\n\tif projectedName == sourceName {\n\t\tt.Fatalf(\"expected projected name to differ: %q was %q\", sourceName, projectedName)\n\t}\n\n\t// trigger the projection\n\tgroup.TriggerAsync()\n\tgroup.TriggerAsync()\n\tgroup.TriggerAsync()\n\n\t// wait until the async trigger has finished\n\twg.Wait()\n\n\t// check that the projected value is updated\n\tif projectedName != sourceName {\n\t\tt.Fatalf(\"expected projected name: %q was %q\", sourceName, projectedName)\n\t}\n}\n\nfunc TestCloseEmptyGroup(t *testing.T) {\n\tg := eventsourcing.NewProjectionGroup()\n\tg.Stop()\n\tg.Start()\n\tg.Stop()\n\tg.Stop()\n}\n\nfunc TestStartMultipleProjections(t *testing.T) {\n\t// setup\n\tes := memory.Create()\n\taggregate.Register(&Person{})\n\n\t// callback that handles the events\n\tcallbackF := func(event eventsourcing.Event) error {\n\t\treturn nil\n\t}\n\n\tr1 := eventsourcing.NewProjection(es.All(0, 1), callbackF)\n\tr2 := eventsourcing.NewProjection(es.All(0, 1), callbackF)\n\tr3 := eventsourcing.NewProjection(es.All(0, 1), callbackF)\n\n\tg := eventsourcing.NewProjectionGroup(r1, r2, r3)\n\tg.Start()\n\tg.Stop()\n}\n\nfunc TestErrorFromCallback(t *testing.T) {\n\t// setup\n\tes := memory.Create()\n\taggregate.Register(&Person{})\n\n\terr := createPersonEvent(es, \"kalle\", 1)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// define application error that can be returned from the callback function\n\tvar ErrApplication = errors.New(\"application error\")\n\n\t// callback that handles the events\n\tcallbackF := func(event eventsourcing.Event) error {\n\t\treturn ErrApplication\n\t}\n\n\tr := eventsourcing.NewProjection(es.All(0, 1), callbackF)\n\n\tg := eventsourcing.NewProjectionGroup(r)\n\n\tg.Start()\n\tdefer g.Stop()\n\n\tselect {\n\tcase err = <-g.ErrChan:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"test timed out\")\n\t}\n\n\tif !errors.Is(err, ErrApplication) {\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"expected application error but got %s\", err.Error())\n\t\t}\n\t\tt.Fatal(\"got none error expected ErrApplication\")\n\t}\n}\n\nfunc TestStrict(t *testing.T) {\n\t// setup\n\tes := memory.Create()\n\tinternal.ResetRegister()\n\n\t// We do not register the Person aggregate with the Born event attached\n\terr := createPersonEvent(es, \"kalle\", 1)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tproj := eventsourcing.NewProjection(es.All(0, 1), func(event eventsourcing.Event) error {\n\t\treturn nil\n\t})\n\n\t_, result := proj.RunOnce()\n\tif !errors.Is(result.Error, eventsourcing.ErrEventNotRegistered) {\n\t\tt.Fatalf(\"expected ErrEventNotRegistered got %q\", err.Error())\n\t}\n}\n\nfunc TestRace(t *testing.T) {\n\t// setup\n\tes := memory.Create()\n\taggregate.Register(&Person{})\n\n\terr := createPersonEvent(es, \"kalle\", 50)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// callback that handles the events\n\tcallbackF := func(event eventsourcing.Event) error {\n\t\ttime.Sleep(time.Millisecond * 2)\n\t\treturn nil\n\t}\n\n\tapplicationErr := errors.New(\"an error\")\n\n\tr1 := eventsourcing.NewProjection(es.All(0, 1), callbackF)\n\tr2 := eventsourcing.NewProjection(es.All(0, 1), func(e eventsourcing.Event) error {\n\t\ttime.Sleep(time.Millisecond)\n\t\tif e.GlobalVersion() == 31 {\n\t\t\treturn applicationErr\n\t\t}\n\t\treturn nil\n\t})\n\n\tresult, err := eventsourcing.ProjectionsRace(true, r1, r2)\n\n\t// causing err should be applicationErr\n\tif !errors.Is(err, applicationErr) {\n\t\tt.Fatalf(\"expected causing error to be applicationErr got %v\", err)\n\t}\n\n\t// projection 0 should have a context.Canceled error\n\tif !errors.Is(result[0].Error, context.Canceled) {\n\t\tt.Fatalf(\"expected projection %q to have err 'context.Canceled' got %v\", result[0].Name, result[0].Error)\n\t}\n\n\t// projection 1 should have a applicationErr error\n\tif !errors.Is(result[1].Error, applicationErr) {\n\t\tt.Fatalf(\"expected projection %q to have err 'applicationErr' got %v\", result[1].Name, result[1].Error)\n\t}\n\n\t// projection 1 should have halted on event with GlobalVersion 30\n\tif result[1].LastHandledEvent.GlobalVersion() != 30 {\n\t\tt.Fatalf(\"expected projection 1 Event.GlobalVersion() to be 30 but was %d\", result[1].LastHandledEvent.GlobalVersion())\n\t}\n}\n\nfunc TestKeepStartPosition(t *testing.T) {\n\t// setup\n\tes := memory.Create()\n\taggregate.Register(&Person{})\n\n\terr := createPersonEvent(es, \"kalle\", 5)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tstart := core.Version(0)\n\tcounter := 0\n\n\t// callback that handles the events\n\tcallbackF := func(event eventsourcing.Event) error {\n\t\tswitch event.Data().(type) {\n\t\tcase *AgedOneYear:\n\t\t\tcounter++\n\t\t}\n\t\tstart = core.Version(event.GlobalVersion() + 1)\n\t\treturn nil\n\t}\n\n\tr := eventsourcing.NewProjection(es.All(0, 1), callbackF)\n\n\t_, err = eventsourcing.ProjectionsRace(true, r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = createPersonEvent(es, \"anka\", 5)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t_, err = eventsourcing.ProjectionsRace(true, r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Born 2 + AgedOnYear 5 + 5 = 12 + Next Event 1 = 13\n\tif start != 13 {\n\t\tt.Fatalf(\"expected start to be 13 was %d\", start)\n\t}\n\n\tif counter != 10 {\n\t\tt.Fatalf(\"expected counter to be 10 was %d\", counter)\n\t}\n}\n"
  },
  {
    "path": "snapshotstore/memory/memory.go",
    "content": "package memory\n\nimport (\n\t\"context\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\ntype Memory struct {\n\tsnapshots map[string]core.Snapshot\n}\n\n// Create in memory snapshot store\nfunc Create() *Memory {\n\treturn &Memory{\n\t\tsnapshots: make(map[string]core.Snapshot),\n\t}\n}\n\nfunc (m *Memory) Close() {\n\n}\n\nfunc (m *Memory) Get(ctx context.Context, aggregateID, aggregateType string) (core.Snapshot, error) {\n\tsnapshot, ok := m.snapshots[aggregateType+\"_\"+aggregateID]\n\tif !ok {\n\t\treturn core.Snapshot{}, core.ErrSnapshotNotFound\n\t}\n\treturn snapshot, nil\n}\n\nfunc (m *Memory) Save(snapshot core.Snapshot) error {\n\tm.snapshots[snapshot.Type+\"_\"+snapshot.ID] = snapshot\n\treturn nil\n}\n"
  },
  {
    "path": "snapshotstore/memory/memory_test.go",
    "content": "package memory_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/core/testsuite\"\n\t\"github.com/hallgren/eventsourcing/snapshotstore/memory\"\n)\n\nfunc TestSuite(t *testing.T) {\n\tf := func() (core.SnapshotStore, func(), error) {\n\t\tss := memory.Create()\n\t\treturn ss, func() { ss.Close() }, nil\n\t}\n\ttestsuite.TestSnapshotStore(t, f)\n}\n"
  },
  {
    "path": "snapshotstore/sql/README.md",
    "content": "# SQL Snapshot Store\n\nThe sql is a module containing multiple sql based snapshot stores that are all based on the\ndatabase/sql interface in go standard library. The different snapshot stores has specific database schemas\nthat support the different databases.\n\n## SQLite\n\nSupports the SQLite database https://www.sqlite.org/\n\n### Database Schema\n\n```go\nCREATE TABLE IF NOT EXISTS snapshots (\n    id              VARCHAR NOT NULL,\n    type            VARCHAR,\n    version         INTEGER,\n    global_version  INTEGER,\n    state           BLOB\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS id_type ON snapshots (id, type);\n```\n\n### Constructor\n\n```go\n// NewSQLite connection to database\nfunc NewSQLite(db *sql.DB) (*SQLite, error) {\n```\n\n### Example of use\n\n```go\n\nimport (\n\tsqldriver \"database/sql\"\n\t\"github.com/hallgren/eventsourcing/snapshotstore/sql\"\n\t_ \"github.com/mattn/go-sqlite3\"\n)\n\ndb, err := sqldriver.Open(\"sqlite3\", \"file::memory:?locked.sqlite?cache=shared\")\nif err != nil {\n\treturn nil, nil, err\n}\n\nsqliteSnapshotStore, err := sql.NewSQLite(db)\nif err != nil {\n\treturn nil, nil, err\n}\n```\n## Postgres\n\nSupports the Postgres database https://www.postgresql.org\n\n### Database Schema\n\n```go\nCREATE TABLE IF NOT EXISTS snapshots (\n    id VARCHAR NOT NULL,\n    type VARCHAR,\n    version INTEGER,\n    global_version INTEGER,\n    state BYTEA\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS id_type ON snapshots (id, type);\n```\n\n### Constructor\n\n```go\n// NewPostgres connection to database\nfunc NewPostgres(db *sql.DB) (*Postgres, error) {\n```\n\n### Example of use\n\n```go\nimport (\n\tgosql \"database/sql\"\n\t_ \"github.com/lib/pq\"\n\t\"github.com/hallgren/eventsourcing/snapshotstore/sql\"\n)\n\ndb, err := gosql.Open(\"postgres\", dsn)\nif err != nil {\n  return err\n}\n\n\npostgreSnapshotStore, err := sql.NewPostgres(db)\nif err != nil {\n\treturn err\n}\n```\n"
  },
  {
    "path": "snapshotstore/sql/go.mod",
    "content": "module github.com/hallgren/eventsourcing/snapshotstore/sql\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/hallgren/eventsourcing/core v0.5.2\n\tgithub.com/lib/pq v1.12.3\n\tgithub.com/mattn/go-sqlite3 v1.14.42\n\tgithub.com/testcontainers/testcontainers-go v0.42.0\n)\n\nrequire (\n\tdario.cat/mergo v1.0.2 // indirect\n\tgithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/containerd/errdefs v1.0.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/containerd/log v0.1.0 // indirect\n\tgithub.com/containerd/platforms v0.2.1 // indirect\n\tgithub.com/cpuguy83/dockercfg v0.3.2 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/docker/go-connections v0.6.0 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/ebitengine/purego v0.10.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-ole/go-ole v1.2.6 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect\n\tgithub.com/magiconair/properties v1.8.10 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/go-archive v0.2.0 // indirect\n\tgithub.com/moby/moby/api v1.54.1 // indirect\n\tgithub.com/moby/moby/client v0.4.0 // indirect\n\tgithub.com/moby/patternmatcher v0.6.1 // indirect\n\tgithub.com/moby/sys/sequential v0.6.0 // indirect\n\tgithub.com/moby/sys/user v0.4.0 // indirect\n\tgithub.com/moby/sys/userns v0.1.0 // indirect\n\tgithub.com/moby/term v0.5.2 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/shirou/gopsutil/v4 v4.26.3 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/stretchr/testify v1.11.1 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect\n\tgo.opentelemetry.io/otel v1.41.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.41.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.41.0 // indirect\n\tgolang.org/x/crypto v0.48.0 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n\n// replace github.com/hallgren/eventsourcing/core => ../../core\n"
  },
  {
    "path": "snapshotstore/sql/go.sum",
    "content": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=\ngithub.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=\ngithub.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=\ngithub.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=\ngithub.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=\ngithub.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hallgren/eventsourcing/core v0.5.2 h1:knvM1jP0zziiybce+Au7ysYvZQnDwxkV+/RFZWNDMiw=\ngithub.com/hallgren/eventsourcing/core v0.5.2/go.mod h1:rgo2kFwNVCb0bzUub5nOPlUYNlFkp1uUQBEQx5fM3Lk=\ngithub.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=\ngithub.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=\ngithub.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=\ngithub.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=\ngithub.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=\ngithub.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=\ngithub.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=\ngithub.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=\ngithub.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=\ngithub.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=\ngithub.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=\ngithub.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=\ngithub.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=\ngithub.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=\ngithub.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=\ngithub.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=\ngithub.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=\ngithub.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=\ngithub.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=\ngithub.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=\ngithub.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=\ngithub.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=\ngithub.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=\ngithub.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=\ngithub.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=\ngithub.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=\ngithub.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=\ngithub.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=\ngithub.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=\ngo.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=\ngo.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=\ngo.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=\ngo.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=\ngo.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=\ngo.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=\ngo.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=\ngo.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=\ngo.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=\ngo.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=\ngolang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=\ngolang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=\ngolang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=\ngolang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\npgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=\npgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=\n"
  },
  {
    "path": "snapshotstore/sql/migrate.go",
    "content": "package sql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n)\n\nfunc migrate(db *sql.DB, stm []string) error {\n\ttx, err := db.BeginTx(context.Background(), nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer tx.Rollback()\n\n\tfor _, b := range stm {\n\t\t_, err := tx.Exec(b)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn tx.Commit()\n}\n"
  },
  {
    "path": "snapshotstore/sql/postgres.go",
    "content": "package sql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\nconst createTablePostgres = `CREATE TABLE IF NOT EXISTS snapshots (\n    id VARCHAR NOT NULL,\n    type VARCHAR,\n    version INTEGER,\n    global_version INTEGER,\n    state BYTEA\n);`\n\nconst createIndexPostgres = `CREATE UNIQUE INDEX IF NOT EXISTS id_type ON snapshots (id, type);`\n\ntype Postgres struct {\n\tdb *sql.DB\n}\n\n// NewPostgres connection to database\nfunc NewPostgres(db *sql.DB) (*Postgres, error) {\n\tif err := migrate(db, []string{\n\t\tcreateTablePostgres,\n\t\tcreateIndexPostgres,\n\t}); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Postgres{\n\t\tdb: db,\n\t}, nil\n}\n\n// Close the connection\nfunc (s *Postgres) Close() {\n\ts.db.Close()\n}\n\n// Save persists the snapshot\nfunc (s *Postgres) Save(snapshot core.Snapshot) error {\n\ttx, err := s.db.BeginTx(context.Background(), nil)\n\tif err != nil {\n\t\treturn errors.New(fmt.Sprintf(\"could not start a write transaction, %v\", err))\n\t}\n\tdefer tx.Rollback()\n\n\tstatement := `SELECT id from snapshots where id=$1 AND type=$2 LIMIT 1`\n\tvar id string\n\terr = tx.QueryRow(statement, snapshot.ID, snapshot.Type).Scan(&id)\n\tif err != nil && err != sql.ErrNoRows {\n\t\treturn err\n\t}\n\tif err == sql.ErrNoRows {\n\t\t// insert\n\t\tstatement = `INSERT INTO snapshots (state, id, type, version, global_version) VALUES ($1, $2, $3, $4, $5)`\n\t\t_, err = tx.Exec(statement, string(snapshot.State), snapshot.ID, snapshot.Type, snapshot.Version, snapshot.GlobalVersion)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\t// update\n\t\tstatement = `UPDATE snapshots set state=$1, version=$2, global_version=$3 where id=$4 AND type=$5`\n\t\t_, err = tx.Exec(statement, string(snapshot.State), snapshot.Version, snapshot.GlobalVersion, snapshot.ID, snapshot.Type)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn tx.Commit()\n}\n\n// Get return the snapshot data from the database\nfunc (s *Postgres) Get(ctx context.Context, aggregateID, aggregateType string) (core.Snapshot, error) {\n\tvar globalVersion core.Version\n\tvar version core.Version\n\tvar state []byte\n\n\tselectStm := `Select version, global_version, state from snapshots where id=$1 and type=$2`\n\trow := s.db.QueryRow(selectStm, aggregateID, aggregateType)\n\tif row.Err() != nil {\n\t\treturn core.Snapshot{}, row.Err()\n\t}\n\terr := row.Scan(&version, &globalVersion, &state)\n\tif err != nil && errors.Is(err, sql.ErrNoRows) {\n\t\treturn core.Snapshot{}, core.ErrSnapshotNotFound\n\t} else if err != nil {\n\t\treturn core.Snapshot{}, err\n\t}\n\n\treturn core.Snapshot{\n\t\tID:            aggregateID,\n\t\tType:          aggregateType,\n\t\tVersion:       version,\n\t\tGlobalVersion: globalVersion,\n\t\tState:         state,\n\t}, nil\n}\n"
  },
  {
    "path": "snapshotstore/sql/postgres_test.go",
    "content": "package sql_test\n\nimport (\n\t\"context\"\n\tgosql \"database/sql\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t_ \"github.com/lib/pq\"\n\t\"github.com/testcontainers/testcontainers-go\"\n\t\"github.com/testcontainers/testcontainers-go/wait\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/core/testsuite\"\n\t\"github.com/hallgren/eventsourcing/snapshotstore/sql\"\n)\n\nfunc TestSuitePostgres(t *testing.T) {\n\tctx := context.Background()\n\n\t// Set up the PostgreSQL container request\n\treq := testcontainers.ContainerRequest{\n\t\tImage:        \"postgres:16\", // Use a specific version\n\t\tExposedPorts: []string{\"5432/tcp\"},\n\t\tEnv: map[string]string{\n\t\t\t\"POSTGRES_USER\":     \"test\",\n\t\t\t\"POSTGRES_PASSWORD\": \"secret\",\n\t\t\t\"POSTGRES_DB\":       \"testdb\",\n\t\t},\n\t\tWaitingFor: wait.ForListeningPort(\"5432/tcp\").WithStartupTimeout(30 * time.Second),\n\t}\n\t// Start the container\n\tpostgresContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{\n\t\tContainerRequest: req,\n\t\tStarted:          true,\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"failed to start container: %v\", err)\n\t}\n\tdefer postgresContainer.Terminate(ctx)\n\n\t// Get container host and port\n\thost, _ := postgresContainer.Host(ctx)\n\tport, _ := postgresContainer.MappedPort(ctx, \"5432\")\n\n\t// Build the DSN\n\tdsn := fmt.Sprintf(\"host=%s port=%s user=test password=secret dbname=testdb sslmode=disable\", host, port.Port())\n\n\tf := func() (core.SnapshotStore, func(), error) {\n\t\t// Connect using database/sql\n\t\tdb, err := gosql.Open(\"postgres\", dsn)\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"db open failed: %w\", err)\n\t\t}\n\t\t// Test the connection\n\t\terr = db.Ping()\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\t\tes, err := sql.NewPostgres(db)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\treturn es, func() {\n\t\t\tdb.Close()\n\t\t}, nil\n\t}\n\ttestsuite.TestSnapshotStore(t, f)\n}\n"
  },
  {
    "path": "snapshotstore/sql/sqlite.go",
    "content": "package sql\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\nconst createTableSQLite = `\nCREATE TABLE IF NOT EXISTS snapshots (\n\tid              VARCHAR NOT NULL,\n\ttype            VARCHAR,\n\tversion         INTEGER,\n\tglobal_version  INTEGER,\n\tstate           BLOB\n);`\n\nconst createIndexSQLite = `CREATE UNIQUE INDEX IF NOT EXISTS id_type ON snapshots (id, type);`\n\ntype SQLite struct {\n\tdb *sql.DB\n}\n\n// NewSQLite connection to database\nfunc NewSQLite(db *sql.DB) (*SQLite, error) {\n\tif err := migrate(db, []string{\n\t\tcreateTableSQLite,\n\t\tcreateIndexSQLite,\n\t}); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &SQLite{\n\t\tdb: db,\n\t}, nil\n}\n\n// Close the connection\nfunc (s *SQLite) Close() {\n\ts.db.Close()\n}\n\n// Save persists the snapshot\nfunc (s *SQLite) Save(snapshot core.Snapshot) error {\n\ttx, err := s.db.BeginTx(context.Background(), nil)\n\tif err != nil {\n\t\treturn errors.New(fmt.Sprintf(\"could not start a write transaction, %v\", err))\n\t}\n\tdefer tx.Rollback()\n\n\tstatement := `SELECT id from snapshots where id=$1 AND type=$2 LIMIT 1`\n\tvar id string\n\terr = tx.QueryRow(statement, snapshot.ID, snapshot.Type).Scan(&id)\n\tif err != nil && err != sql.ErrNoRows {\n\t\treturn err\n\t}\n\tif err == sql.ErrNoRows {\n\t\t// insert\n\t\tstatement = `INSERT INTO snapshots (state, id, type, version, global_version) VALUES ($1, $2, $3, $4, $5)`\n\t\t_, err = tx.Exec(statement, string(snapshot.State), snapshot.ID, snapshot.Type, snapshot.Version, snapshot.GlobalVersion)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\t// update\n\t\tstatement = `UPDATE snapshots set state=$1, version=$2, global_version=$3 where id=$4 AND type=$5`\n\t\t_, err = tx.Exec(statement, string(snapshot.State), snapshot.Version, snapshot.GlobalVersion, snapshot.ID, snapshot.Type)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn tx.Commit()\n}\n\n// Get return the snapshot data from the database\nfunc (s *SQLite) Get(ctx context.Context, aggregateID, aggregateType string) (core.Snapshot, error) {\n\tvar globalVersion core.Version\n\tvar version core.Version\n\tvar state []byte\n\n\tselectStm := `Select version, global_version, state from snapshots where id=? and type=?`\n\trow := s.db.QueryRow(selectStm, aggregateID, aggregateType)\n\tif row.Err() != nil {\n\t\treturn core.Snapshot{}, row.Err()\n\t}\n\terr := row.Scan(&version, &globalVersion, &state)\n\tif err != nil && errors.Is(err, sql.ErrNoRows) {\n\t\treturn core.Snapshot{}, core.ErrSnapshotNotFound\n\t} else if err != nil {\n\t\treturn core.Snapshot{}, err\n\t}\n\n\treturn core.Snapshot{\n\t\tID:            aggregateID,\n\t\tType:          aggregateType,\n\t\tVersion:       version,\n\t\tGlobalVersion: globalVersion,\n\t\tState:         state,\n\t}, nil\n}\n"
  },
  {
    "path": "snapshotstore/sql/sqlite_test.go",
    "content": "package sql_test\n\nimport (\n\tsqldriver \"database/sql\"\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/core/testsuite\"\n\t\"github.com/hallgren/eventsourcing/snapshotstore/sql\"\n\t_ \"github.com/mattn/go-sqlite3\"\n)\n\nfunc TestSuite(t *testing.T) {\n\tf := func() (core.SnapshotStore, func(), error) {\n\t\treturn snapshotstore()\n\t}\n\ttestsuite.TestSnapshotStore(t, f)\n}\n\nfunc snapshotstore() (*sql.SQLite, func(), error) {\n\tdb, err := sqldriver.Open(\"sqlite3\", \"file::memory:?locked.sqlite?cache=shared\")\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tdb.SetMaxOpenConns(1)\n\terr = db.Ping()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tstore, err := sql.NewSQLite(db)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn store, func() {\n\t\tstore.Close()\n\t}, nil\n}\n"
  }
]