Showing preview only (265K chars total). Download the full file or copy to clipboard to get everything.
Repository: hallgren/eventsourcing
Branch: master
Commit: c3821dcdc7ce
Files: 81
Total size: 245.2 KB
Directory structure:
gitextract_nsi1zgi3/
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ └── go.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── aggregate/
│ ├── aggregate.go
│ ├── aggregate_test.go
│ ├── idgenerator.go
│ ├── root.go
│ ├── root_test.go
│ ├── snapshot.go
│ └── snapshot_test.go
├── core/
│ ├── event.go
│ ├── eventstore.go
│ ├── fetcher.go
│ ├── go.mod
│ ├── snapshotstore.go
│ └── testsuite/
│ ├── eventstoresuite.go
│ ├── fetcher.go
│ └── snapshotstoresuite.go
├── event.go
├── eventsourcing.go
├── eventsourcing_test.go
├── eventstore/
│ ├── bbolt/
│ │ ├── README.md
│ │ ├── bbolt.go
│ │ ├── bbolt_test.go
│ │ ├── go.mod
│ │ ├── go.sum
│ │ └── iterator.go
│ ├── esdb/
│ │ ├── README.md
│ │ ├── esdb.go
│ │ ├── esdb_test.go
│ │ ├── go.mod
│ │ ├── go.sum
│ │ └── iterator.go
│ ├── kurrent/
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── iterator.go
│ │ ├── kurrent.go
│ │ └── kurrent_test.go
│ ├── memory/
│ │ ├── iterator.go
│ │ ├── memory.go
│ │ └── memory_test.go
│ └── sql/
│ ├── README.md
│ ├── go.mod
│ ├── go.sum
│ ├── iterator.go
│ ├── migrate.go
│ ├── postgres.go
│ ├── postgres_test.go
│ ├── sqlite.go
│ ├── sqlite_test.go
│ ├── sqlserver.go
│ └── sqlserver_test.go
├── example/
│ ├── README.md
│ ├── go.mod
│ ├── go.sum
│ ├── order/
│ │ ├── cmd/
│ │ │ └── main.go
│ │ ├── order.go
│ │ └── order_test.go
│ └── tictactoe/
│ ├── cmd/
│ │ └── main/
│ │ └── main.go
│ ├── tictactoe.go
│ └── tictactoe_test.go
├── go.mod
├── go.sum
├── internal/
│ ├── encoderjson.go
│ └── register.go
├── iterator.go
├── projections.go
├── projections_test.go
└── snapshotstore/
├── memory/
│ ├── memory.go
│ └── memory_test.go
└── sql/
├── README.md
├── go.mod
├── go.sum
├── migrate.go
├── postgres.go
├── postgres_test.go
├── sqlite.go
└── sqlite_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
# eventstore
- package-ecosystem: "gomod"
directory: "/eventstore/kurrent/"
schedule:
interval: "weekly"
- package-ecosystem: "gomod"
directory: "/eventstore/esdb/"
schedule:
interval: "weekly"
- package-ecosystem: "gomod"
directory: "/eventstore/bbolt/"
schedule:
interval: "weekly"
- package-ecosystem: "gomod"
directory: "/eventstore/sql/"
schedule:
interval: "weekly"
# snapshotstore
- package-ecosystem: "gomod"
directory: "/snapshotstore/sql/"
schedule:
interval: "weekly"
================================================
FILE: .github/workflows/go.yml
================================================
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
name: Go
permissions:
contents: read
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
main:
name: Main module
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.19'
- name: Build
run: go build -v ./...
- name: Test
run: go test -v -race ./...
bbolt:
name: bbolt eventstore
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Build
run: cd eventstore/bbolt && go build -v ./...
- name: Test
run: cd eventstore/bbolt && go test -v -race ./...
sql:
name: sql eventstore
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Build
run: cd eventstore/sql && go build -v ./...
- name: Test
run: cd eventstore/sql && go test -v -race ./...
esdb:
name: esdb eventstore
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Build
run: cd eventstore/esdb && go build -v ./...
- name: Test
run: cd eventstore/esdb && go test -v -race ./...
kurrent:
name: kurrent eventstore
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Build
run: cd eventstore/kurrent && go build -v ./...
- name: Test
run: cd eventstore/kurrent && go test -v -race ./...
sqlsnapshot:
name: sql snapshotstore
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Build
run: cd snapshotstore/sql && go build -v ./...
- name: Test
run: cd snapshotstore/sql && go test -v -race ./...
================================================
FILE: .gitignore
================================================
example/example
.idea
coverage.txt
*.swp
go.work
================================================
FILE: LICENSE
================================================
__________________________________
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
================================================
FILE: Makefile
================================================
all:
go fmt ./...
go build
#core
cd core && go build
# event stores
cd eventstore/bbolt && go build
cd eventstore/sql && go build
cd eventstore/esdb && go build
test:
#core
cd core && go test -count 1 ./...
# event stores
cd eventstore/bbolt && go test -count 1 ./...
cd eventstore/sql && go test -count 1 ./...
cd eventstore/esdb && go test esdb_test.go -count 1 ./...
# main
go test -count 1 ./...
update:
# event stores
cd eventstore/bbolt && go get -t -u ./... && go mod tidy
cd eventstore/sql && go get -u ./... && go mod tidy
cd eventstore/esdb && go get -t -u ./... && go mod tidy
#snaptshot stores
cd snapshotstore/sql && go get -u ./... && go mod tidy
# main
go get -t -u ./... && go mod tidy
================================================
FILE: README.md
================================================
# Overview
This 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).
It's structured in two main parts:
* [Aggregate](https://github.com/hallgren/eventsourcing?tab=readme-ov-file#aggregate) - Model and Load/Save aggregates (write side).
* [Consuming events](https://github.com/hallgren/eventsourcing?tab=readme-ov-file#projections) - Handle events and build read-models (read side).
## Event Sourcing
[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.
### Aggregate
The *aggregate* is the central point where events are bound. The aggregate struct needs to embed `aggregate.Root` to get the aggregate behaviors.
*Person* aggregate where the Aggregate Root is embedded next to the `Name` and `Age` properties.
```go
type Person struct {
aggregate.Root
Name string
Age int
}
```
The aggregate needs to implement the `Transition(event eventsourcing.Event)` and `Register(r aggregate.RegisterFunc)` methods to fulfill the aggregate interface.
These methods define how events are transformed to build the aggregate state and which events to register into the repository.
Example of the Transition method on the `Person` aggregate.
```go
// Transition the person state dependent on the events
func (person *Person) Transition(event eventsourcing.Event) {
switch e := event.Data().(type) {
case *Born:
person.Age = 0
person.Name = e.Name
case *AgedOneYear:
person.Age += 1
}
}
```
The `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.
Example or the Register method:
```go
// Register callback method that register Person events to the repository
func (person *Person) Register(r aggregate.RegisterFunc) {
r(&Born{}, &AgedOneYear{})
}
```
The `Born` and `AgedOneYear` events are now registered to the repository when the aggregate is registered.
### Event
An event is a clean struct with exported properties that contains the state of the event.
Example of two events from the `Person` aggregate.
```go
// Initial event
type Born struct {
Name string
}
// Event that happens once a year
type AgedOneYear struct {}
```
When an aggregate is first created, an event is needed to initialize the state of the aggregate. No event, no aggregate.
Example of a constructor that returns the `Person` aggregate and inside it binds an event via the `aggregate.TrackChange` function.
It'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.
```go
// CreatePerson constructor for Person
func CreatePerson(name string) (*Person, error) {
if name == "" {
return nil, errors.New("name can't be blank")
}
person := Person{}
aggregate.TrackChange(&person, &Born{Name: name})
return &person, nil
}
```
When 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`.
```go
// GrowOlder command
func (person *Person) GrowOlder() {
aggregate.TrackChange(person, &AgedOneYear{})
}
```
Internally the `aggregate.TrackChange` function calls the `Transition` method on the aggregate to transform the aggregate based on the newly created event.
To bind metadata to events use the `aggregate.TrackChangeWithMetadata` function.
The `Event` has the following behaviours..
```go
type Event struct {
// aggregate identifier
AggregateID() string
// the aggregate version when this event was created
Version() Version
// the global version is based on all events (this value is only set after the event is saved to the event store)
GlobalVersion() Version
// aggregate type (Person in the example above)
AggregateType() string
// UTC time when the event was created
Timestamp() time.Time
// the specific event data specified in the application (Born{}, AgedOneYear{})
Data() interface{}
// data that don´t belongs to the application state (could be correlation id or other request references)
Metadata() map[string]interface{}
}
```
### Aggregate ID
The 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.
* Set a specific id on the aggregate via the SetID func.
```go
var id = "123"
person := Person{}
err := person.SetID(id)
```
* Change the id generator via the global aggregate.SetIDFunc function.
```go
var counter = 0
f := func() string {
counter++
return fmt.Sprint(counter)
}
aggregate.SetIDFunc(f)
```
## Save/Load Aggregate
To 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.
The `aggregate` interface is automatically applied on the application defined aggregate when `aggregate.Root` is embedded.
```go
// Save stores the aggregate events in the supplied event store
aggregate.Save(es core.EventStore, a aggregate) error
// Load returns the aggregate based on its events
aggregate.Load(ctx context.Context, es core.EventStore, id string, a aggregate) error
```
To 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
the `aggregate.Register` function.
```go
// the person aggregate has to be registered in the repository
aggregate.Register(&Person{})
```
### Event Store
The only thing an event store handles are events, and it must implement the following interface.
```go
// saves events to the underlaying data store.
Save(events []core.Event) error
// 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.
Get(id string, aggregateType string, afterVersion core.Version) (core.Iterator, error)
```
There are four implementations in this repository.
* [SQL](https://github.com/hallgren/eventsourcing/blob/master/eventstore/sql/README.md) - `go get github.com/hallgren/eventsourcing/eventstore/sql`
* SQLite
* Postgres
* Microsoft SQL Server
* Bolt - `go get github.com/hallgren/eventsourcing/eventstore/bbolt`
* Event Store DB - `go get github.com/hallgren/eventsourcing/eventstore/esdb`
* Kurrent DB - `go get github.com/hallgren/eventsourcing/eventstore/kurrent`
* RAM Memory - part of the main module
External event stores:
* [DynamoDB](https://github.com/fd1az/dynamo-es) by [fd1az](https://github.com/fd1az)
* [SQL pgx driver](https://github.com/CentralConcept/go-eventsourcing-pgx/tree/main/eventstore/pgx)
### Custom event store
If 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`
interface to support the eventsourcing.EventRepository.
```go
type EventStore interface {
Save(events []core.Event) error
Get(id string, aggregateType string, afterVersion core.Version) (core.Iterator, error)
}
```
The event store needs to import the `github.com/hallgren/eventsourcing/core` module that expose the `core.Event`, `core.Version` and `core.Iterator` types.
### Encoder
Before 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`.
When an event is fetched the encoder deserialize the `Data` and `Metadata` `[]byte` back into their actual types.
The 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:
```go
type Encoder interface {
Serialize(v interface{}) ([]byte, error)
Deserialize(data []byte, v interface{}) error
}
```
### Realtime Event Subscription
For 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.
## Snapshot
If 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.
The 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
used to build the aggregate. The use of snapshots is optional and is exposed via the snapshot functions on the aggregate package.
### Save/Load Snapshot
It's only possible to save a snapshot if it has no pending events, meaning that the aggregate events are saved before saving the snapshot.
```go
// Saves a snapshot
aggregate.SaveSnapshot(ss core.SnapshotStore, s snapshot) error
// Loads the aggregate only from the snapshot state not adding events that were saved after the snapshot was taken
aggregate.LoadSnapshot(ctx context.Context, ss core.SnapshotStore, id string, s snapshot) error
// Loads the aggregate from the snapshot and also adds events
aggregate.LoadFromSnapshot(ctx context.Context, es core.EventStore, ss core.SnapshotStore, id string, as aggregateSnapshot) error
```
### Snapshot Store
Like the event store's the snapshot repository is built on the same design. The snapshot store has to implement the following methods.
```go
type SnapshotStore interface {
Save(snapshot Snapshot) error
Get(ctx context.Context, id, aggregateType string) (Snapshot, error)
}
```
There are two implementations in this repository.
* [SQL](https://github.com/hallgren/eventsourcing/blob/master/snapshotstore/sql/README.md) - `go get github.com/hallgren/eventsourcing/snapshotstore/sql`
* SQLite
* Postgres
* RAM Memory - part of the main module
External event stores:
* [SQL pgx driver](https://github.com/CentralConcept/go-eventsourcing-pgx/tree/main/snapshotstore/pgx)
### Unexported aggregate properties
As unexported properties on a struct are not possible to serialize there is the same limitation on aggregates.
To fix this there are optional callback methods that can be added to the aggregate struct.
```go
type snapshot interface {
SerializeSnapshot(SerializeFunc) ([]byte, error)
DeserializeSnapshot(DeserializeFunc, []byte) error
}
```
Example:
```go
// aggregate
type Person struct {
aggregate.Root
unexported string
}
// snapshot struct
type PersonSnapshot struct {
UnExported string
}
// callback that maps the aggregate to the snapshot struct with the exported property
func (s *Person) SerializeSnapshot(m aggregate.SnapshotMarshal) ([]byte, error) {
snap := PersonSnapshot{
Unexported: s.unexported,
}
return m(snap)
}
// callback to map the snapshot back to the aggregate
func (s *Person) DeserializeSnapshot(m aggregate.SnapshotUnmarshal, b []byte) error {
snap := PersonSnapshot{}
err := m(b, &snap)
if err != nil {
return err
}
s.unexported = snap.UnExported
return nil
}
```
It's possible to change the default json encoder by the `eventsourcing.SetSnapshotEncoder(e Encoder)` function.
## Projections
Projections 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.
If 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).
### Projection
A _projection_ is created from the `eventsourcing.NewProjection` function. The method takes a `core.Fetcher` and a `callbackFunc` and returns a pointer to the projection.
```go
p := pr.Projection(f core.Fetcher, c callbackFunc)
```
The core.Fetcher type `func() (core.Iterator, error)`, i.e it return the same signature that event stores return when they return events.
```go
type Fetcher func() (core.Iterator, error)
```
The `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.
```go
type callbackFunc func(e eventsourcing.Event) error
```
Example: Creates a projection that fetches all events from an event store and handle them in the callbackF.
```go
p := eventsourcing.NewProjection(es.All(0, 1), func(event eventsourcing.Event) error {
switch e := event.Data().(type) {
case *Born:
// handle the event
}
return nil
})
```
### Projection execution
A projection can be started in three different ways.
#### RunOnce
RunOnce fetch events from the event store one time. It returns true if there were events to iterate otherwise false.
```go
RunOnce() (bool, ProjectionResult)
```
#### RunToEnd
RunToEnd 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.
```go
RunToEnd(ctx context.Context) ProjectionResult
```
`RunOnce` and `RunToEnd` both return a ProjectionResult
```go
type ProjectionResult struct {
Error error
ProjectionName string
LastHandledEvent Event
}
```
* **Error** Is set if the projection returned an error
* **ProjectionName** Is the name of the projection
* **LastHandledEvent** The last successfully handled event (can be useful during debugging)
#### Run
Run 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`.
```go
Run(ctx context.Context, pace time.Duration) error
```
A running projection can be triggered manually via `TriggerAsync()` or `TriggerSync()`.
### Projection properties
A projection has a set of properties that can affect its behavior.
* **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.
* **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.
### Run multiple projections
#### Group
A set of projections can run concurrently in a group.
```go
g := eventsourcing.NewProjectionGroup(p1, p2, p3)
```
A 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`.
The `g.Stop()` method is used to halt all projections in the group and it returns when all projections have stopped.
```go
// create three projections
p1 := eventsourcing.NewProjection(es.All(0, 1), callbackF)
p2 := eventsourcing.NewProjection(es.All(0, 1), callbackF)
p3 := eventsourcing.NewProjection(es.All(0, 1), callbackF)
// create a group containing the projections
g := eventsourcing.NewProjectionGroup(p1, p2, p3)
// Start runs all projections concurrently
g.Start()
// Stop terminate all projections and wait for them to return
defer g.Stop()
// handling error in projection or termination from outside
select {
case err := <-g.ErrChan:
// handle the error
case <-doneChan:
// stop signal from the out side
return
}
```
The pace of the projection can be changed with the `Pace` property. Default is every 10 seconds.
If the pace is not fast enough for some scenario it's possible to trigger manually.
`TriggerAsync()`: Triggers all projections in the group and returns.
`TriggerSync()`: Triggers all projections in the group and waits for them running to the end of their event streams.
#### Race
Compared 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.
The `Race()` method starts the projections and runs them to the end of their event streams. When all projections are finished the method returns.
```go
eventsourcing.ProjectionsRace(cancelOnError bool, projections ...*Projection) ([]ProjectionResult, error)
```
If `cancelOnError` is set to true the method will halt all projections and return if any projection is returning an error.
The returned `[]ProjectionResult` is a collection of all projection results.
Race example:
```go
// create two projections
p1 := eventsourcing.NewProjection(es.All(0, 1), callbackF)
p2 := eventsourcing.NewProjection(es.All(0, 1), callbackF)
// true make the race return on error in any projection
result, err := eventsourcing.ProjectionsRace(true, r1, r2)
```
================================================
FILE: aggregate/aggregate.go
================================================
package aggregate
import (
"context"
"errors"
"fmt"
"reflect"
"github.com/hallgren/eventsourcing"
"github.com/hallgren/eventsourcing/core"
"github.com/hallgren/eventsourcing/internal"
)
type RegisterFunc = func(events ...interface{})
// Aggregate interface to use the aggregate root specific methods
type aggregate interface {
root() *Root
Transition(event eventsourcing.Event)
Register(RegisterFunc)
}
// Load returns the aggregate based on its events
func Load(ctx context.Context, es core.EventStore, id string, a aggregate) error {
if reflect.ValueOf(a).Kind() != reflect.Ptr {
return eventsourcing.ErrAggregateNeedsToBeAPointer
}
root := a.root()
iterator, err := getEvents(ctx, es, id, aggregateType(a), root.Version())
if err != nil {
return err
}
defer iterator.Close()
for iterator.Next() {
select {
case <-ctx.Done():
return ctx.Err()
default:
event, err := iterator.Value()
if err != nil {
return err
}
buildFromHistory(a, []eventsourcing.Event{event})
}
}
if root.Version() == 0 {
return eventsourcing.ErrAggregateNotFound
}
return nil
}
// LoadFromSnapshot fetch the aggregate by first get its snapshot and later append events after the snapshot was stored
// This can speed up the load time of aggregates with many events
func LoadFromSnapshot(ctx context.Context, es core.EventStore, ss core.SnapshotStore, id string, as aggregateSnapshot) error {
err := LoadSnapshot(ctx, ss, id, as)
if err != nil {
return err
}
return Load(ctx, es, id, as)
}
// Save stores the aggregate events in the supplied event store
func Save(es core.EventStore, a aggregate) error {
root := a.root()
// return as quick as possible when no events to process
if len(root.events) == 0 {
return nil
}
if !internal.GlobalRegister.AggregateRegistered(a) {
return fmt.Errorf("%s %w", aggregateType(a), eventsourcing.ErrAggregateNotRegistered)
}
globalVersion, err := saveEvents(es, root.Events())
if err != nil {
return err
}
// update the global version on the aggregate
root.globalVersion = globalVersion
// set internal properties and reset the events slice
lastEvent := root.events[len(root.events)-1]
root.version = lastEvent.Version()
root.events = []eventsourcing.Event{}
return nil
}
// Register registers the aggregate and its events
func Register(a aggregate) {
internal.GlobalRegister.Register(a)
}
// Save events to the event store
func saveEvents(eventStore core.EventStore, events []eventsourcing.Event) (eventsourcing.Version, error) {
var esEvents = make([]core.Event, 0, len(events))
for _, event := range events {
data, err := internal.EventEncoder.Serialize(event.Data())
if err != nil {
return 0, err
}
metadata, err := internal.EventEncoder.Serialize(event.Metadata())
if err != nil {
return 0, err
}
esEvent := core.Event{
AggregateID: event.AggregateID(),
Version: core.Version(event.Version()),
AggregateType: event.AggregateType(),
Timestamp: event.Timestamp(),
Data: data,
Metadata: metadata,
Reason: event.Reason(),
}
_, ok := internal.GlobalRegister.EventRegistered(esEvent)
if !ok {
return 0, fmt.Errorf("%s %w", esEvent.Reason, eventsourcing.ErrEventNotRegistered)
}
esEvents = append(esEvents, esEvent)
}
err := eventStore.Save(esEvents)
if err != nil {
if errors.Is(err, core.ErrConcurrency) {
return 0, eventsourcing.ErrConcurrency
}
return 0, fmt.Errorf("error from event store: %w", err)
}
return eventsourcing.Version(esEvents[len(esEvents)-1].GlobalVersion), nil
}
// getEvents return event iterator based on aggregate inputs from the event store
func getEvents(ctx context.Context, eventStore core.EventStore, id, aggregateType string, fromVersion eventsourcing.Version) (*eventsourcing.Iterator, error) {
// fetch events after the current version of the aggregate that could be fetched from the snapshot store
eventIterator, err := eventStore.Get(ctx, id, aggregateType, core.Version(fromVersion))
if err != nil {
return nil, err
}
return &eventsourcing.Iterator{
CoreIterator: eventIterator,
}, nil
}
================================================
FILE: aggregate/aggregate_test.go
================================================
package aggregate_test
import (
"context"
"testing"
"github.com/hallgren/eventsourcing"
"github.com/hallgren/eventsourcing/aggregate"
"github.com/hallgren/eventsourcing/eventstore/memory"
ss "github.com/hallgren/eventsourcing/snapshotstore/memory"
)
func TestSaveAndLoadAggregate(t *testing.T) {
es := memory.Create()
aggregate.Register(&Person{})
person, err := CreatePerson("kalle")
if err != nil {
t.Fatal(err)
}
err = aggregate.Save(es, person)
if err != nil {
t.Fatalf("could not save aggregate, err: %v", err)
}
// make sure the global version is set to 1
if person.GlobalVersion() != 1 {
t.Fatalf("global version is: %d expected: 1", person.GlobalVersion())
}
twin := Person{}
err = aggregate.Load(context.Background(), es, person.ID(), &twin)
if err != nil {
t.Fatalf("could not get aggregate err: %v", err)
}
// Check internal aggregate version
if person.Version() != twin.Version() {
t.Fatalf("Wrong version org %q copy %q", person.Version(), twin.Version())
}
// Check person Name
if person.Name != twin.Name {
t.Fatalf("Wrong Name org %q copy %q", person.Name, twin.Name)
}
}
func TestLoadAggregateFromSnapshot(t *testing.T) {
es := memory.Create()
ss := ss.Create()
aggregate.Register(&Person{})
person, err := CreatePerson("kalle")
if err != nil {
t.Fatal(err)
}
err = aggregate.Save(es, person)
if err != nil {
t.Fatalf("could not save aggregate, err: %v", err)
}
// store snapshot
err = aggregate.SaveSnapshot(ss, person)
if err != nil {
t.Fatal(err)
}
// add one more event to the person aggregate
person.GrowOlder()
err = aggregate.Save(es, person)
// load person to person2 from snaphost and events
person2 := &Person{}
err = aggregate.LoadFromSnapshot(context.Background(), es, ss, person.ID(), person2)
if err != nil {
t.Fatal(err)
}
if person.Age != person2.Age {
t.Fatalf("expected same age on person(%d) and person2(%d)", person.Age, person2.Age)
}
}
func TestLoadNoneExistingAggregate(t *testing.T) {
es := memory.Create()
aggregate.Register(&Person{})
p := Person{}
err := aggregate.Load(context.Background(), es, "none_existing", &p)
if err != eventsourcing.ErrAggregateNotFound {
t.Fatal("could not get aggregate")
}
}
================================================
FILE: aggregate/idgenerator.go
================================================
package aggregate
import (
"crypto/rand"
)
// idFunc is a global function that generates aggregate id's.
// It could be changed from the outside via the SetIDFunc function.
var idFunc = randSeq
// SetIDFunc is used to change how aggregate ID's are generated
// default is a random string
func SetIDFunc(f func() string) {
idFunc = f
}
func randSeq() string {
id, err := generateRandomString(20)
if err != nil {
return ""
}
return id
}
func generateRandomString(n int) (string, error) {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
bytes, err := generateRandomBytes(n)
if err != nil {
return "", err
}
for i, b := range bytes {
bytes[i] = letters[b%byte(len(letters))]
}
return string(bytes), nil
}
func generateRandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
return b, err
}
================================================
FILE: aggregate/root.go
================================================
package aggregate
import (
"reflect"
"time"
"github.com/hallgren/eventsourcing"
"github.com/hallgren/eventsourcing/core"
)
// Root to be included into aggregates to give it the aggregate root behaviors
type Root struct {
id string
version eventsourcing.Version
globalVersion eventsourcing.Version
events []eventsourcing.Event
}
const emptyID = ""
// TrackChange is used internally by behaviour methods to apply a state change to
// the current instance and also track it in order that it can be persisted later.
func TrackChange(a aggregate, data interface{}) {
TrackChangeWithMetadata(a, data, nil)
}
// TrackChangeWithMetadata is used internally by behaviour methods to apply a state change to
// the current instance and also track it in order that it can be persisted later.
// meta data is handled by this func to store none related application state
func TrackChangeWithMetadata(a aggregate, data interface{}, metadata map[string]interface{}) {
ar := a.root()
// This can be overwritten in the constructor of the aggregate
if ar.id == emptyID {
ar.id = idFunc()
}
event := eventsourcing.NewEvent(
core.Event{
AggregateID: ar.id,
Version: ar.nextVersion(),
AggregateType: aggregateType(a),
Timestamp: time.Now().UTC(),
},
data,
metadata,
)
ar.events = append(ar.events, event)
a.Transition(event)
}
// buildFromHistory builds the aggregate state from events
func buildFromHistory(a aggregate, events []eventsourcing.Event) {
root := a.root()
for _, event := range events {
a.Transition(event)
//Set the aggregate ID
root.id = event.AggregateID()
// Make sure the aggregate is in the correct version (the last event)
root.version = event.Version()
root.globalVersion = event.GlobalVersion()
}
}
func (ar *Root) nextVersion() core.Version {
return core.Version(ar.Version()) + 1
}
// SetID opens up the possibility to set manual aggregate ID from the outside
func (ar *Root) SetID(id string) error {
if ar.id != emptyID {
return eventsourcing.ErrAggregateAlreadyExists
}
ar.id = id
return nil
}
// ID returns the aggregate ID as a string
func (ar *Root) ID() string {
return ar.id
}
// root returns the included Aggregate Root state, and is used from the interface Aggregate.
//
//nolint:unused
func (ar *Root) root() *Root {
return ar
}
// Version return the version based on events that are not stored
func (ar *Root) Version() eventsourcing.Version {
if len(ar.events) > 0 {
return ar.events[len(ar.events)-1].Version()
}
return eventsourcing.Version(ar.version)
}
// GlobalVersion returns the global version based on the last stored event
func (ar *Root) GlobalVersion() eventsourcing.Version {
return eventsourcing.Version(ar.globalVersion)
}
// Events return the aggregate events from the aggregate
// make a copy of the slice preventing outsiders modifying events.
//
//nolint:gosimple // for some reason copy does not work
func (ar *Root) Events() []eventsourcing.Event {
e := make([]eventsourcing.Event, len(ar.events))
// convert internal event to external event
for i, event := range ar.events {
e[i] = event
}
return e
}
// UnsavedEvents return true if there's unsaved events on the aggregate
func (ar *Root) UnsavedEvents() bool {
return len(ar.events) > 0
}
func aggregateType(a interface{}) string {
return reflect.TypeOf(a).Elem().Name()
}
================================================
FILE: aggregate/root_test.go
================================================
package aggregate_test
import (
"encoding/json"
"errors"
"fmt"
"testing"
"time"
"github.com/hallgren/eventsourcing"
"github.com/hallgren/eventsourcing/aggregate"
)
// Person aggregate
type Person struct {
aggregate.Root
Name string
Age int
Dead int
}
// Born event
type Born struct {
Name string
}
// AgedOneYear event
type AgedOneYear struct {
}
// CreatePerson constructor for the Person
func CreatePerson(name string) (*Person, error) {
if name == "" {
return nil, errors.New("name can't be blank")
}
person := Person{}
aggregate.TrackChange(&person, &Born{Name: name})
return &person, nil
}
// CreatePersonWithID constructor for the Person that sets the aggregate ID from the outside
func CreatePersonWithID(id, name string) (*Person, error) {
if name == "" {
return nil, errors.New("name can't be blank")
}
person := Person{}
err := person.SetID(id)
if err == eventsourcing.ErrAggregateAlreadyExists {
return nil, err
} else if err != nil {
return nil, err
}
aggregate.TrackChange(&person, &Born{Name: name})
return &person, nil
}
// GrowOlder command
func (person *Person) GrowOlder() {
metaData := make(map[string]interface{})
metaData["foo"] = "bar"
aggregate.TrackChangeWithMetadata(person, &AgedOneYear{}, metaData)
}
// Register bind the events to the repository when the aggregate is registered.
func (person *Person) Register(f aggregate.RegisterFunc) {
f(&Born{}, &AgedOneYear{})
}
// Transition the person state dependent on the events
func (person *Person) Transition(event eventsourcing.Event) {
switch e := event.Data().(type) {
case *Born:
person.Age = 0
person.Name = e.Name
case *AgedOneYear:
person.Age += 1
}
}
func (person *Person) SerializeSnapshot(aggregate.SnapshotMarshal) ([]byte, error) {
return json.Marshal(person)
}
func (person *Person) DeserializeSnapshot(f aggregate.SnapshotUnmarshal, d []byte) error {
return json.Unmarshal(d, person)
}
func TestPersonWithNoEvents(t *testing.T) {
person := Person{}
if person.Version() != 0 {
t.Fatalf("should have version 0 had %d", person.Version())
}
}
func TestCreateNewPerson(t *testing.T) {
timeBefore := time.Now().UTC()
person, err := CreatePerson("kalle")
if err != nil {
t.Fatal("Error when creating person", err.Error())
}
if person.Name != "kalle" {
t.Fatal("Wrong person Name")
}
if person.Age != 0 {
t.Fatal("Wrong person Age")
}
if len(person.Events()) != 1 {
t.Fatal("There should be one event on the person aggregateRoot")
}
if person.Version() != 1 {
t.Fatal("Wrong version on the person aggregateRoot", person.Version())
}
if person.Events()[0].Timestamp().Before(timeBefore) {
t.Fatal("event timestamp before timeBefore")
}
if person.Events()[0].Timestamp().After(time.Now().UTC()) {
t.Fatal("event timestamp after current time")
}
if person.Events()[0].GlobalVersion() != 0 {
t.Fatalf("global version should not be set when event is created, was %d", person.Events()[0].GlobalVersion())
}
if !person.UnsavedEvents() {
t.Fatal("there should be event on the aggregate")
}
}
func TestCreateNewPersonWithIDFromOutside(t *testing.T) {
id := "123"
person, err := CreatePersonWithID(id, "kalle")
if err != nil {
t.Fatal("Error when creating person", err.Error())
}
if person.ID() != id {
t.Fatal("Wrong aggregate ID on the person aggregateRoot", person.ID())
}
}
func TestBlankName(t *testing.T) {
_, err := CreatePerson("")
if err == nil {
t.Fatal("The constructor should return error on blank Name")
}
}
func TestSetIDOnExistingPerson(t *testing.T) {
person, err := CreatePerson("Kalle")
if err != nil {
t.Fatal("The constructor returned error")
}
err = person.SetID("new_id")
if err == nil {
t.Fatal("Should not be possible to set ID on already existing person")
}
}
func TestPersonAgedOneYear(t *testing.T) {
person, _ := CreatePerson("kalle")
person.GrowOlder()
if len(person.Events()) != 2 {
t.Fatal("There should be two event on the person aggregateRoot", person.Events())
}
if person.Events()[len(person.Events())-1].Reason() != "AgedOneYear" {
t.Fatal("The last event reason should be AgedOneYear", person.Events()[len(person.Events())-1].Reason())
}
d, ok := person.Events()[1].Metadata()["foo"]
if !ok {
t.Fatal("meta data not present")
}
if d.(string) != "bar" {
t.Fatal("wrong meta data")
}
if person.ID() == "" {
t.Fatal("aggregate ID should not be empty")
}
}
func TestPersonGrewTenYears(t *testing.T) {
person, _ := CreatePerson("kalle")
for i := 1; i <= 10; i++ {
person.GrowOlder()
}
if person.Age != 10 {
t.Fatal("person has the wrong Age")
}
}
func TestSetIDFunc(t *testing.T) {
var counter = 0
f := func() string {
counter++
return fmt.Sprint(counter)
}
aggregate.SetIDFunc(f)
for i := 1; i < 10; i++ {
person, _ := CreatePerson("kalle")
if person.ID() != fmt.Sprint(i) {
t.Fatalf("id not set via the new SetIDFunc, exp: %d got: %s", i, person.ID())
}
}
}
func TestIDFuncGeneratingRandomIDs(t *testing.T) {
var ids = map[string]struct{}{}
for i := 1; i < 100000; i++ {
person, _ := CreatePerson("kalle")
_, exists := ids[person.ID()]
if exists {
t.Fatalf("id: %s, already created", person.ID())
}
ids[person.ID()] = struct{}{}
}
}
================================================
FILE: aggregate/snapshot.go
================================================
package aggregate
import (
"context"
"errors"
"reflect"
"github.com/hallgren/eventsourcing"
"github.com/hallgren/eventsourcing/core"
"github.com/hallgren/eventsourcing/internal"
)
type SnapshotMarshal func(v interface{}) ([]byte, error)
type SnapshotUnmarshal func(data []byte, v interface{}) error
// snapshot interface is used to serialize an aggregate that has properties that are not exported
type snapshot interface {
root() *Root
SerializeSnapshot(f SnapshotMarshal) ([]byte, error)
DeserializeSnapshot(f SnapshotUnmarshal, d []byte) error
}
type aggregateSnapshot interface {
aggregate
snapshot
}
// LoadSnapshot build the aggregate based on its snapshot data not including its events.
// Beware that it could be more events that has happened after the snapshot was taken
func LoadSnapshot(ctx context.Context, ss core.SnapshotStore, id string, s snapshot) error {
if reflect.ValueOf(s).Kind() != reflect.Ptr {
return eventsourcing.ErrAggregateNeedsToBeAPointer
}
err := getSnapshot(ctx, ss, id, s)
if err != nil && errors.Is(err, core.ErrSnapshotNotFound) {
return eventsourcing.ErrAggregateNotFound
}
return err
}
func getSnapshot(ctx context.Context, ss core.SnapshotStore, id string, s snapshot) error {
snap, err := ss.Get(ctx, id, aggregateType(s))
if err != nil {
return err
}
err = s.DeserializeSnapshot(internal.SnapshotEncoder.Deserialize, snap.State)
if err != nil {
return err
}
// set the internal aggregate properties
root := s.root()
root.globalVersion = eventsourcing.Version(snap.GlobalVersion)
root.version = eventsourcing.Version(snap.Version)
root.id = snap.ID
return nil
}
// SaveSnapshot will only store the snapshot and will return an error if there are events that are not stored
func SaveSnapshot(ss core.SnapshotStore, s snapshot) error {
root := s.root()
if len(root.Events()) > 0 {
return eventsourcing.ErrUnsavedEvents
}
state := []byte{}
var err error
state, err = s.SerializeSnapshot(internal.SnapshotEncoder.Serialize)
if err != nil {
return err
}
snapshot := core.Snapshot{
ID: root.ID(),
Type: aggregateType(s),
Version: core.Version(root.Version()),
GlobalVersion: core.Version(root.GlobalVersion()),
State: state,
}
return ss.Save(snapshot)
}
================================================
FILE: aggregate/snapshot_test.go
================================================
package aggregate_test
import (
"context"
"errors"
"testing"
"github.com/hallgren/eventsourcing"
"github.com/hallgren/eventsourcing/aggregate"
"github.com/hallgren/eventsourcing/core"
"github.com/hallgren/eventsourcing/eventstore/memory"
snap "github.com/hallgren/eventsourcing/snapshotstore/memory"
)
func createPerson() *Person {
es := memory.Create()
aggregate.Register(&Person{})
person, err := CreatePerson("kalle")
if err != nil {
panic(err)
}
aggregate.Save(es, person)
return person
}
func TestSaveAndGetSnapshot(t *testing.T) {
snapshotStore := snap.Create()
person := createPerson()
err := aggregate.SaveSnapshot(snapshotStore, person)
if err != nil {
t.Fatalf("could not save aggregate, err: %v", err)
}
twin := Person{}
err = aggregate.LoadSnapshot(context.Background(), snapshotStore, person.ID(), &twin)
if err != nil {
t.Fatalf("could not get aggregate, err: %v", err)
}
// Check internal aggregate version
if person.Version() != twin.Version() {
t.Fatalf("Wrong version org %q copy %q", person.Version(), twin.Version())
}
if person.ID() != twin.ID() {
t.Fatalf("Wrong id org %q copy %q", person.ID(), twin.ID())
}
if person.Name != twin.Name {
t.Fatalf("Wrong name org: %q copy %q", person.Name, twin.Name)
}
}
func TestGetNoneExistingSnapshotOrEvents(t *testing.T) {
snapshotStore := snap.Create()
person := Person{}
err := aggregate.LoadSnapshot(context.Background(), snapshotStore, "none_existing_id", &person)
if !errors.Is(err, eventsourcing.ErrAggregateNotFound) {
t.Fatal("should get error when no snapshot or event stored for aggregate")
}
}
func TestGetNoneExistingSnapshot(t *testing.T) {
snapshotStore := snap.Create()
person := Person{}
err := aggregate.LoadSnapshot(context.Background(), snapshotStore, "none_existing_id", &person)
if !errors.Is(err, eventsourcing.ErrAggregateNotFound) {
t.Fatal("should get error when no snapshot stored for aggregate")
}
}
func TestSaveSnapshotWithUnsavedEvents(t *testing.T) {
snapshotStore := snap.Create()
person, err := CreatePerson("kalle")
if err != nil {
t.Fatal(err)
}
err = aggregate.SaveSnapshot(snapshotStore, person)
if err == nil {
t.Fatalf("should not be able to save snapshot with unsaved events")
}
}
// test custom snapshot struct to handle non-exported properties on aggregate
type snapshot struct {
aggregate.Root
unexported string
Exported string
// to be able to save the snapshot after events are added to it.
repo core.EventStore
}
type Event struct{}
type Event2 struct{}
func New() *snapshot {
es := memory.Create()
aggregate.Register(&snapshot{})
s := snapshot{}
aggregate.TrackChange(&s, &Event{})
s.repo = es
aggregate.Save(es, &s)
return &s
}
func (s *snapshot) Command() {
aggregate.TrackChange(s, &Event2{})
aggregate.Save(s.repo, s)
}
func (s *snapshot) Transition(e eventsourcing.Event) {
switch e.Data().(type) {
case *Event:
s.unexported = "unexported"
s.Exported = "Exported"
case *Event2:
s.unexported = "unexported2"
s.Exported = "Exported2"
}
}
// Register bind the events to the repository when the aggregate is registered.
func (s *snapshot) Register(f aggregate.RegisterFunc) {
f(&Event{}, &Event2{})
}
type snapshotInternal struct {
UnExported string
Exported string
}
func (s *snapshot) SerializeSnapshot(f aggregate.SnapshotMarshal) ([]byte, error) {
snap := snapshotInternal{
UnExported: s.unexported,
Exported: s.Exported,
}
return f(snap)
}
func (s *snapshot) DeserializeSnapshot(f aggregate.SnapshotUnmarshal, b []byte) error {
snap := snapshotInternal{}
err := f(b, &snap)
if err != nil {
return err
}
s.unexported = snap.UnExported
s.Exported = snap.Exported
return nil
}
func TestSnapshotNoneExported(t *testing.T) {
snapshotStore := snap.Create()
snap := New()
err := aggregate.SaveSnapshot(snapshotStore, snap)
if err != nil {
t.Fatal(err)
}
snap.Command()
err = aggregate.SaveSnapshot(snapshotStore, snap)
if err != nil {
t.Fatal(err)
}
snap2 := snapshot{}
err = aggregate.LoadSnapshot(context.Background(), snapshotStore, snap.ID(), &snap2)
if err != nil {
t.Fatal(err)
}
if snap.unexported != snap2.unexported {
t.Fatalf("none exported value differed %s %s", snap.unexported, snap2.unexported)
}
if snap.Exported != snap2.Exported {
t.Fatalf("exported value differed %s %s", snap.Exported, snap2.Exported)
}
}
================================================
FILE: core/event.go
================================================
package core
import (
"time"
)
// Version is the event version used in event.Version and event.GlobalVersio
type Version uint64
// Event holding meta data and the application specific event in the Data property
type Event struct {
AggregateID string
Version Version
GlobalVersion Version
AggregateType string
Timestamp time.Time
Reason string // based on the Data type
Data []byte // interface{} on the external Event type
Metadata []byte // map[string]interface{} on the external Event type
}
================================================
FILE: core/eventstore.go
================================================
package core
import (
"context"
"errors"
)
// ErrConcurrency when the currently saved version of the aggregate differs from the new ones
var ErrConcurrency = errors.New("concurrency error")
// Iterator is the interface an event store Get needs to return
type Iterator interface {
Next() bool
Value() (Event, error)
Close()
}
// EventStore interface expose the methods an event store must uphold
type EventStore interface {
Save(events []Event) error
Get(ctx context.Context, id string, aggregateType string, afterVersion Version) (Iterator, error)
}
================================================
FILE: core/fetcher.go
================================================
package core
// Fetcher is the event fetch function concumed by projections
type Fetcher func() (Iterator, error)
================================================
FILE: core/go.mod
================================================
module github.com/hallgren/eventsourcing/core
go 1.13
================================================
FILE: core/snapshotstore.go
================================================
package core
import (
"context"
"errors"
)
// ErrSnapshotNotFound returned when no snapshot is found in the snapshot store
var ErrSnapshotNotFound = errors.New("snapshot not found")
// Snapshot holds current state of an aggregate
type Snapshot struct {
ID string
Type string
Version Version
GlobalVersion Version
State []byte
}
// SnapshotStore expose the methods a snapshot store must uphold
type SnapshotStore interface {
Save(snapshot Snapshot) error
Get(ctx context.Context, id, aggregateType string) (Snapshot, error)
}
================================================
FILE: core/testsuite/eventstoresuite.go
================================================
package testsuite
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/rand"
"sync"
"testing"
"time"
"github.com/hallgren/eventsourcing/core"
)
var seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
func AggregateID() string {
r := seededRand.Intn(999999999999)
return fmt.Sprintf("%d", r)
}
type eventstoreFunc = func() (core.EventStore, func(), error)
// Status represents the Red, Silver or Gold tier level of a FrequentFlierAccount
type Status int
const (
StatusRed Status = iota
StatusSilver Status = iota
StatusGold Status = iota
)
type FrequentFlierAccountCreated struct {
AccountId string
OpeningMiles int
OpeningTierPoints int
}
type StatusMatched struct {
NewStatus Status
}
type FlightTaken struct {
MilesAdded int
TierPointsAdded int
}
var aggregateType = "FrequentFlierAccount"
var timestamp = time.Now()
func eventToByte(i interface{}) []byte {
b, _ := json.Marshal(i)
return b
}
func testEvents(aggregateID string) []core.Event {
metadata := make(map[string]interface{})
metadata["test"] = "hello"
history := []core.Event{
{AggregateID: aggregateID, Version: 1, AggregateType: aggregateType, Timestamp: timestamp, Reason: "FrequentFlierAccountCreated", Data: eventToByte(&FrequentFlierAccountCreated{AccountId: "1234567", OpeningMiles: 10000, OpeningTierPoints: 0}), Metadata: eventToByte(metadata)},
{AggregateID: aggregateID, Version: 2, AggregateType: aggregateType, Timestamp: timestamp, Reason: "StatusMatched", Data: eventToByte(&StatusMatched{NewStatus: StatusSilver}), Metadata: eventToByte(metadata)},
{AggregateID: aggregateID, Version: 3, AggregateType: aggregateType, Timestamp: timestamp, Reason: "FlightTaken", Data: eventToByte(&FlightTaken{MilesAdded: 2525, TierPointsAdded: 5}), Metadata: eventToByte(metadata)},
{AggregateID: aggregateID, Version: 4, AggregateType: aggregateType, Timestamp: timestamp, Reason: "FlightTaken", Data: eventToByte(&FlightTaken{MilesAdded: 2512, TierPointsAdded: 5}), Metadata: eventToByte(metadata)},
{AggregateID: aggregateID, Version: 5, AggregateType: aggregateType, Timestamp: timestamp, Reason: "FlightTaken", Data: eventToByte(&FlightTaken{MilesAdded: 5600, TierPointsAdded: 5}), Metadata: eventToByte(metadata)},
{AggregateID: aggregateID, Version: 6, AggregateType: aggregateType, Timestamp: timestamp, Reason: "FlightTaken", Data: eventToByte(&FlightTaken{MilesAdded: 3000, TierPointsAdded: 3}), Metadata: eventToByte(metadata)},
}
return history
}
func testEventsPartTwo(aggregateID string) []core.Event {
history := []core.Event{
{AggregateID: aggregateID, Version: 7, AggregateType: aggregateType, Timestamp: timestamp, Reason: "FlightTaken", Data: eventToByte(&FlightTaken{MilesAdded: 5600, TierPointsAdded: 5})},
{AggregateID: aggregateID, Version: 8, AggregateType: aggregateType, Timestamp: timestamp, Reason: "FlightTaken", Data: eventToByte(&FlightTaken{MilesAdded: 3000, TierPointsAdded: 3})},
}
return history
}
func testEventOtherAggregate(aggregateID string) core.Event {
return core.Event{AggregateID: aggregateID, Version: 1, AggregateType: aggregateType, Timestamp: timestamp, Reason: "FrequentFlierAccountCreated", Data: eventToByte(&FrequentFlierAccountCreated{AccountId: "1234567", OpeningMiles: 10000, OpeningTierPoints: 0})}
}
func Test(t *testing.T, esFunc eventstoreFunc) {
tests := []struct {
title string
run func(es core.EventStore) error
}{
{"should save and get events", saveAndGetEvents},
{"should get events after version", getEventsAfterVersion},
{"should not save events in wrong version", saveEventsInWrongVersion},
{"should save and get event concurrently", saveAndGetEventsConcurrently},
{"should return error when no events", getErrWhenNoEvents},
{"should get global event order from save", saveReturnGlobalEventOrder},
}
for _, test := range tests {
t.Run(test.title, func(t *testing.T) {
es, closeFunc, err := esFunc()
if err != nil {
t.Fatal(err)
}
err = test.run(es)
if err != nil {
// make use of t.Error instead of t.Fatal to make sure the closeFunc is executed
t.Error(err)
}
closeFunc()
})
}
}
func saveAndGetEvents(es core.EventStore) error {
aggregateID := AggregateID()
events := testEvents(aggregateID)
fetchedEvents := []core.Event{}
err := es.Save(events)
if err != nil {
return err
}
iterator, err := es.Get(context.Background(), aggregateID, aggregateType, 0)
if err != nil {
return err
}
for iterator.Next() {
event, err := iterator.Value()
if err != nil {
return err
}
fetchedEvents = append(fetchedEvents, event)
}
iterator.Close()
if len(fetchedEvents) != len(testEvents(aggregateID)) {
return errors.New("wrong number of events returned")
}
if fetchedEvents[0].Version != testEvents(aggregateID)[0].Version {
return errors.New("wrong events returned")
}
// Add more events to the same aggregate event stream
err = es.Save(testEventsPartTwo(aggregateID))
if err != nil {
return err
}
fetchedEventsIncludingPartTwo := []core.Event{}
iterator, err = es.Get(context.Background(), aggregateID, aggregateType, 0)
if err != nil {
return err
}
for iterator.Next() {
event, err := iterator.Value()
if err != nil {
break
}
fetchedEventsIncludingPartTwo = append(fetchedEventsIncludingPartTwo, event)
}
iterator.Close()
if len(fetchedEventsIncludingPartTwo) != len(append(testEvents(aggregateID), testEventsPartTwo(aggregateID)...)) {
return errors.New("wrong number of events returned")
}
if fetchedEventsIncludingPartTwo[0].Version != testEvents(aggregateID)[0].Version {
return errors.New("wrong event version returned")
}
if fetchedEventsIncludingPartTwo[0].AggregateID != testEvents(aggregateID)[0].AggregateID {
return errors.New("wrong event aggregateID returned")
}
if fetchedEventsIncludingPartTwo[0].AggregateType != testEvents(aggregateID)[0].AggregateType {
return errors.New("wrong event aggregateType returned")
}
if fetchedEventsIncludingPartTwo[0].Reason != testEvents(aggregateID)[0].Reason {
return errors.New("wrong event reason returned")
}
return nil
}
func getEventsAfterVersion(es core.EventStore) error {
var fetchedEvents []core.Event
aggregateID := AggregateID()
err := es.Save(testEvents(aggregateID))
if err != nil {
return err
}
iterator, err := es.Get(context.Background(), aggregateID, aggregateType, 1)
if err != nil {
return err
}
for iterator.Next() {
event, err := iterator.Value()
if err != nil {
break
}
fetchedEvents = append(fetchedEvents, event)
}
iterator.Close()
// Should return one less event
if len(fetchedEvents) != len(testEvents(aggregateID))-1 {
return fmt.Errorf("wrong number of events returned exp: %d, got:%d", len(fetchedEvents), len(testEvents(aggregateID))-1)
}
// first event version should be 2
if fetchedEvents[0].Version != 2 {
return fmt.Errorf("wrong events returned")
}
return nil
}
func saveEventsInWrongVersion(es core.EventStore) error {
aggregateID := AggregateID()
events := testEventsPartTwo(aggregateID)
err := es.Save(events)
if !errors.Is(err, core.ErrConcurrency) {
return errors.New("should not be able to save events that are out of sync compared to the storage order")
}
return nil
}
func saveAndGetEventsConcurrently(es core.EventStore) error {
wg := sync.WaitGroup{}
var err error
aggregateID := AggregateID()
wg.Add(10)
for i := 0; i < 10; i++ {
events := testEvents(fmt.Sprintf("%s-%d", aggregateID, i))
go func() {
e := es.Save(events)
if e != nil {
err = e
}
wg.Done()
}()
}
if err != nil {
return err
}
wg.Wait()
wg.Add(10)
for i := 0; i < 10; i++ {
eventID := fmt.Sprintf("%s-%d", aggregateID, i)
go func() {
defer wg.Done()
iterator, e := es.Get(context.Background(), eventID, aggregateType, 0)
if e != nil {
err = e
return
}
events := make([]core.Event, 0)
for iterator.Next() {
event, err := iterator.Value()
if err != nil {
break
}
events = append(events, event)
}
iterator.Close()
if len(events) != 6 {
err = fmt.Errorf("wrong number of events fetched, expecting 6 got %d", len(events))
return
}
}()
}
if err != nil {
return err
}
wg.Wait()
return nil
}
func getErrWhenNoEvents(es core.EventStore) error {
aggregateID := AggregateID()
iterator, err := es.Get(context.Background(), aggregateID, aggregateType, 0)
if err != nil {
return err
}
defer iterator.Close()
if iterator.Next() {
return fmt.Errorf("expect no event when no events are saved")
}
return nil
}
func saveReturnGlobalEventOrder(es core.EventStore) error {
aggregateID := AggregateID()
aggregateID2 := AggregateID()
events := testEvents(aggregateID)
err := es.Save(events)
if err != nil {
return err
}
if events[len(events)-1].GlobalVersion == 0 {
return fmt.Errorf("expected global event order > 0 on last event got %d", events[len(events)-1].GlobalVersion)
}
events2 := []core.Event{testEventOtherAggregate(aggregateID2)}
err = es.Save(events2)
if err != nil {
return err
}
if events2[0].GlobalVersion <= events[len(events)-1].GlobalVersion {
return fmt.Errorf("expected larger global event order got %d", events2[0].GlobalVersion)
}
return nil
}
/* re-activate when esdb eventstore have global event order on each stream
func setGlobalVersionOnSavedEvents(es eventsourcing.EventStore) error {
events := testEvents()
err := es.Save(events)
if err != nil {
return err
}
eventsGet, err := es.Get(events[0].AggregateID, events[0].AggregateType, 0)
if err != nil {
return err
}
var g eventsourcing.Version
for _, e := range eventsGet {
g++
if e.GlobalVersion != g {
return fmt.Errorf("expected global version to be in sequens exp: %d, was: %d", g, e.GlobalVersion)
}
}
return nil
}
*/
================================================
FILE: core/testsuite/fetcher.go
================================================
package testsuite
import (
"fmt"
"testing"
"github.com/hallgren/eventsourcing/core"
)
func TestFetcher(t *testing.T, es core.EventStore, fetcher core.Fetcher) {
globalVersion := core.Version(0)
aggregateID := AggregateID()
events := testEvents(aggregateID)
err := es.Save(events)
if err != nil {
t.Fatal(err)
}
iter, err := fetcher()
if err != nil {
t.Fatal(err)
}
err = verify(iter, globalVersion, events)
if err != nil {
iter.Close()
t.Fatal(err)
}
iter.Close()
// set the globalVersion to the length of the events as not all event stores update the
// globalVersion on the events after they are saved
globalVersion = core.Version(len(events))
events2 := testEventsPartTwo(aggregateID)
err = es.Save(events2)
if err != nil {
t.Fatal(err)
}
// run fetcher second time - should not restart from first event
iter2, err := fetcher()
if err != nil {
t.Fatal(err)
}
defer iter2.Close()
err = verify(iter2, globalVersion, events2)
if err != nil {
t.Fatal(err)
}
}
// verifies that the events from the iterator has a globalversion higher than sent in globalVersion and that
// the events are the same from the iterator and sent in event.
func verify(iter core.Iterator, globalVersion core.Version, expEvents []core.Event) error {
i := 0
for iter.Next() {
event, err := iter.Value()
if err != nil {
return err
}
if event.Version != expEvents[i].Version && event.AggregateID != expEvents[i].AggregateID {
return 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)
}
if event.GlobalVersion <= globalVersion {
return fmt.Errorf("event global version (%q) is lower than previos event %q", event.GlobalVersion, globalVersion)
}
globalVersion = event.GlobalVersion
i++
}
if i != len(expEvents) {
return fmt.Errorf("expected %d events got %d", len(expEvents), i)
}
return nil
}
================================================
FILE: core/testsuite/snapshotstoresuite.go
================================================
package testsuite
import (
"context"
"errors"
"fmt"
"testing"
"github.com/hallgren/eventsourcing/core"
)
type snapshotstoreFunc = func() (core.SnapshotStore, func(), error)
func TestSnapshotStore(t *testing.T, ssFunc snapshotstoreFunc) {
tests := []struct {
title string
run func(es core.SnapshotStore) error
}{
{"should save and get snapshot", saveAndGetSnapshot},
{"should get error when getting none existing snapshot", getNoneExistingSnapshot},
}
for _, test := range tests {
t.Run(test.title, func(t *testing.T) {
ss, closeFunc, err := ssFunc()
if err != nil {
t.Fatal(err)
}
err = test.run(ss)
if err != nil {
// make use of t.Error instead of t.Fatal to make sure the closeFunc is executed
t.Error(err)
}
closeFunc()
})
}
}
func saveAndGetSnapshot(ss core.SnapshotStore) error {
snapshot := core.Snapshot{
ID: "id",
Type: "person",
Version: 1,
GlobalVersion: 1,
State: []byte("123"),
}
err := ss.Save(snapshot)
if err != nil {
return err
}
s, err := ss.Get(context.Background(), "id", "person")
if err != nil {
return err
}
if string(s.State) != "123" {
return errors.New("wrong snapshot state")
}
if s.Version != snapshot.Version {
return fmt.Errorf("exp version %d got %d", snapshot.Version, s.Version)
}
if s.GlobalVersion != snapshot.GlobalVersion {
return fmt.Errorf("exp global version %d got %d", snapshot.GlobalVersion, s.GlobalVersion)
}
s, err = ss.Get(context.Background(), "none_existing_id", "person")
if !errors.Is(err, core.ErrSnapshotNotFound) {
return err
}
return nil
}
func getNoneExistingSnapshot(ss core.SnapshotStore) error {
_, err := ss.Get(context.Background(), "id", "person")
if !errors.Is(err, core.ErrSnapshotNotFound) {
return err
}
return nil
}
================================================
FILE: event.go
================================================
package eventsourcing
import (
"reflect"
"time"
"github.com/hallgren/eventsourcing/core"
)
// Version is the event version used in event.Version and event.GlobalVersion
type Version core.Version
type Event struct {
event core.Event // internal event
data interface{}
metadata map[string]interface{}
}
func NewEvent(e core.Event, data interface{}, metadata map[string]interface{}) Event {
return Event{event: e, data: data, metadata: metadata}
}
func (e Event) Data() interface{} {
return e.data
}
func (e Event) Metadata() map[string]interface{} {
return e.metadata
}
func (e Event) AggregateType() string {
return e.event.AggregateType
}
func (e Event) AggregateID() string {
return e.event.AggregateID
}
func (e Event) Reason() string {
if e.data == nil {
return ""
}
return reflect.TypeOf(e.data).Elem().Name()
}
func (e Event) Version() Version {
return Version(e.event.Version)
}
func (e Event) Timestamp() time.Time {
return e.event.Timestamp
}
func (e Event) GlobalVersion() Version {
return Version(e.event.GlobalVersion)
}
================================================
FILE: eventsourcing.go
================================================
package eventsourcing
import (
"errors"
"github.com/hallgren/eventsourcing/internal"
)
var (
// ErrAggregateNotFound returns if events not found for aggregate or aggregate was not based on snapshot from the outside
ErrAggregateNotFound = errors.New("aggregate not found")
// ErrAggregateNotRegistered when saving aggregate when it's not registered in the repository
ErrAggregateNotRegistered = errors.New("aggregate not registered")
// ErrEventNotRegistered when saving aggregate and one event is not registered in the repository
ErrEventNotRegistered = errors.New("event not registered")
// ErrConcurrency when the currently saved version of the aggregate differs from the new events
ErrConcurrency = errors.New("concurrency error")
// ErrAggregateAlreadyExists returned if the aggregateID is set more than one time
ErrAggregateAlreadyExists = errors.New("its not possible to set ID on already existing aggregate")
// ErrAggregateNeedsToBeAPointer return if aggregate is sent in as value object
ErrAggregateNeedsToBeAPointer = errors.New("aggregate needs to be a pointer")
// ErrUnsavedEvents aggregate events must be saved before creating snapshot
ErrUnsavedEvents = errors.New("aggregate holds unsaved events")
)
// Encoder is the interface used to Serialize/Deserialize events and snapshots
type Encoder interface {
Serialize(v interface{}) ([]byte, error)
Deserialize(data []byte, v interface{}) error
}
// SetEventEncoder change the default JSON encoder that serialize/deserialize events
func SetEventEncoder(e Encoder) {
internal.EventEncoder = e
}
// SetSnapshotEncoder change the default JSON encoder that seialize/deserialize snapshots
func SetSnapshotEncoder(e Encoder) {
internal.SnapshotEncoder = e
}
================================================
FILE: eventsourcing_test.go
================================================
package eventsourcing_test
/*
func TestGetWithContextCancel(t *testing.T) {
es := memory.Create()
aggregate.AggregateRegister(&Person{})
person, err := CreatePerson("kalle")
if err != nil {
t.Fatal(err)
}
err = aggregate.AggregateSave(es, person)
if err != nil {
t.Fatal("could not save aggregate")
}
ctx, cancel := context.WithCancel(context.Background())
// cancel the context
cancel()
err = eventsourcing.AggregateLoad(ctx, es, person.ID(), person)
if !errors.Is(err, context.Canceled) {
t.Fatalf("expected error context.Canceled but was %v", err)
}
}
func TestSaveWhenAggregateNotRegistered(t *testing.T) {
es := memory.Create()
eventsourcing.ResetRegsiter()
person, err := CreatePerson("kalle")
if err != nil {
t.Fatal(err)
}
err = eventsourcing.AggregateSave(es, person)
if !errors.Is(err, eventsourcing.ErrAggregateNotRegistered) {
t.Fatalf("could save aggregate that was not registered, err: %v", err)
}
}
func TestMultipleSave(t *testing.T) {
es := memory.Create()
eventsourcing.AggregateRegister(&Person{})
person, err := CreatePerson("kalle")
if err != nil {
t.Fatal(err)
}
err = eventsourcing.AggregateSave(es, person)
if err != nil {
t.Fatalf("could not save aggregate, err: %v", err)
}
version := person.Version()
err = eventsourcing.AggregateSave(es, person)
if err != nil {
t.Fatalf("save should be a nop, err: %v", err)
}
if version != person.Version() {
t.Fatalf("the nop save should not change the aggregate version exp:%d, actual:%d", version, person.Version())
}
}
func TestConcurrentRead(t *testing.T) {
es := memory.Create()
eventsourcing.AggregateRegister(&Person{})
person, err := CreatePerson("kalle")
if err != nil {
t.Fatal(err)
}
err = eventsourcing.AggregateSave(es, person)
if err != nil {
t.Fatal("could not save aggregate")
}
person2, err := CreatePerson("anka")
if err != nil {
t.Fatal(err)
}
err = eventsourcing.AggregateSave(es, person2)
if err != nil {
t.Fatal("could not save aggregate")
}
for i := 1; i <= 1000; i++ {
p1 := Person{}
p2 := Person{}
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
eventsourcing.AggregateLoad(context.Background(), es, person.ID(), &p1)
wg.Done()
}()
go func() {
eventsourcing.AggregateLoad(context.Background(), es, person2.ID(), &p2)
wg.Done()
}()
wg.Wait()
if p1.Name == p2.Name {
t.Fatal("name should differ")
}
}
}
*/
================================================
FILE: eventstore/bbolt/README.md
================================================
## BBolt Event Store
This event store supports the go.etcd.io/bbolt bolt driver.
### Constructor
```go
// New opens the event stream found in the given file. If the file is not found it will be created and
// initialized. Will return error if it has problems persisting the changes to the filesystem.
func New(dbFile string) (*BBolt, error) {
```
### Example of use
```go
import "github.com/hallgren/eventsourcing/eventstore/bbolt"
bboltEventStore, err := bbolt.New("bolt.db")
if err != nil {
return nil, nil, err
}
```
================================================
FILE: eventstore/bbolt/bbolt.go
================================================
package bbolt
import (
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/hallgren/eventsourcing/core"
"go.etcd.io/bbolt"
)
const (
globalEventOrderBucketName = "global_event_order"
)
// BBolt is the eventstore handler
type BBolt struct {
db *bbolt.DB // The bbolt db where we store everything
}
type boltEvent struct {
AggregateID string
Version uint64
GlobalVersion uint64
Reason string
AggregateType string
Timestamp time.Time
Data []byte
Metadata []byte // map[string]interface{}
}
// New opens the event stream found in the given file. If the file is not found it will be created and
// initialized. Will return error if it has problems persisting the changes to the filesystem.
func New(dbFile string) (*BBolt, error) {
db, err := bbolt.Open(dbFile, 0600, &bbolt.Options{
Timeout: 1 * time.Second,
})
if err != nil {
return nil, err
}
// Ensure that we have a bucket to store the global event ordering
err = db.Update(func(tx *bbolt.Tx) error {
if _, err := tx.CreateBucketIfNotExists([]byte(globalEventOrderBucketName)); err != nil {
return errors.New("could not create global event order bucket")
}
return nil
})
if err != nil {
return nil, err
}
return &BBolt{
db: db,
}, nil
}
// Save an aggregate (its events)
func (e *BBolt) Save(events []core.Event) error {
// Return if there is no events to save
if len(events) == 0 {
return nil
}
// get bucket name from first event
aggregateType := events[0].AggregateType
aggregateID := events[0].AggregateID
bucketRef := bucketRef(aggregateType, aggregateID)
tx, err := e.db.Begin(true)
if err != nil {
return err
}
defer tx.Rollback()
evBucket := tx.Bucket(bucketRef)
if evBucket == nil {
// Ensure that we have a bucket named events_aggregateType_aggregateID for the given aggregate
err = e.createBucket(bucketRef, tx)
if err != nil {
return errors.New("could not create aggregate events bucket")
}
evBucket = tx.Bucket(bucketRef)
}
currentVersion := uint64(0)
cursor := evBucket.Cursor()
k, obj := cursor.Last()
if k != nil {
lastEvent := boltEvent{}
err := json.Unmarshal(obj, &lastEvent)
if err != nil {
return errors.New(fmt.Sprintf("could not serialize event, %v", err))
}
currentVersion = lastEvent.Version
}
// Make sure no other has saved event to the same aggregate concurrently
if core.Version(currentVersion)+1 != events[0].Version {
return core.ErrConcurrency
}
globalBucket := tx.Bucket([]byte(globalEventOrderBucketName))
if globalBucket == nil {
return errors.New("global bucket not found")
}
var globalSequence uint64
for i, event := range events {
sequence, err := evBucket.NextSequence()
if err != nil {
return errors.New(fmt.Sprintf("could not get sequence for %#v", string(bucketRef)))
}
// We need to establish a global event order that spans over all buckets. This is so that we can be
// able to play the event (or send) them in the order that they was entered into this database.
// The global sequence bucket contains an ordered line of pointer to all events on the form bucket_name:seq_num
globalSequence, err = globalBucket.NextSequence()
if err != nil {
return errors.New("could not get next sequence for global bucket")
}
// build the internal bolt event
bEvent := boltEvent{
AggregateID: event.AggregateID,
AggregateType: event.AggregateType,
Version: uint64(event.Version),
GlobalVersion: globalSequence,
Reason: event.Reason,
Timestamp: event.Timestamp,
Metadata: event.Metadata,
Data: event.Data,
}
value, err := json.Marshal(bEvent)
if err != nil {
return errors.New(fmt.Sprintf("could not serialize event, %v", err))
}
err = evBucket.Put(itob(sequence), value)
if err != nil {
return errors.New(fmt.Sprintf("could not save event %#v in bucket", event))
}
err = globalBucket.Put(itob(globalSequence), value)
if err != nil {
return errors.New(fmt.Sprintf("could not save global sequence pointer for %#v", string(bucketRef)))
}
// override the event in the slice exposing the GlobalVersion to the caller
events[i].GlobalVersion = core.Version(globalSequence)
}
return tx.Commit()
}
// Get aggregate events
func (e *BBolt) Get(ctx context.Context, id string, aggregateType string, afterVersion core.Version) (core.Iterator, error) {
tx, err := e.db.Begin(false)
if err != nil {
return nil, err
}
bucket := tx.Bucket(bucketRef(aggregateType, id))
if bucket == nil {
return &Iterator{tx: tx}, nil
}
cursor := bucket.Cursor()
return &Iterator{tx: tx, cursor: cursor, startPosition: position(afterVersion)}, nil
}
// All iterate over event in GlobalEvents order
func (e *BBolt) All(start core.Version) core.Fetcher {
iter := Iterator{}
return func() (core.Iterator, error) {
tx, err := e.db.Begin(false)
if err != nil {
return nil, err
}
bucket := tx.Bucket([]byte(globalEventOrderBucketName))
if bucket == nil {
return &Iterator{tx: tx}, nil
}
// set start from second call and forward
if iter.CurrentGlobalVersion != 0 {
// dont add 1 to CurrentGlobalVersion as the index is zero based
start = iter.CurrentGlobalVersion
}
cursor := bucket.Cursor()
iter.tx = tx
iter.cursor = cursor
iter.startPosition = position(core.Version(start))
return &iter, nil
//return &iter{tx: tx, cursor: cursor, startPosition: position(core.Version(start))}, nil
}
}
// Close closes the event stream and the underlying database
func (e *BBolt) Close() error {
return e.db.Close()
}
// CreateBucket creates a bucket
func (e *BBolt) createBucket(bucketRef []byte, tx *bbolt.Tx) error {
// Ensure that we have a bucket named event_type for the given type
if _, err := tx.CreateBucketIfNotExists(bucketRef); err != nil {
return errors.New(fmt.Sprintf("could not create bucket for %s: %s", string(bucketRef), err))
}
return nil
}
// bucketRef return the reference where to store and fetch events
func bucketRef(aggregateType, aggregateID string) []byte {
return []byte(aggregateType + "_" + aggregateID)
}
// calculate the correct posiotion and convert to bbolt key type
func position(p core.Version) []byte {
return itob(uint64(p + 1))
}
// itob returns an 8-byte big endian representation of v.
func itob(v uint64) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, v)
return b
}
================================================
FILE: eventstore/bbolt/bbolt_test.go
================================================
package bbolt_test
import (
"os"
"testing"
"github.com/hallgren/eventsourcing/core"
"github.com/hallgren/eventsourcing/core/testsuite"
"github.com/hallgren/eventsourcing/eventstore/bbolt"
)
func TestEventStoreSuite(t *testing.T) {
f := func() (core.EventStore, func(), error) {
dbFile := "bolt.db"
es, err := bbolt.New(dbFile)
if err != nil {
return nil, nil, err
}
return es, func() {
es.Close()
os.Remove(dbFile)
}, nil
}
testsuite.Test(t, f)
}
func TestFetchFuncAll(t *testing.T) {
dbFile := "bolt.db"
es, err := bbolt.New(dbFile)
if err != nil {
t.Fatal(err)
}
defer func() {
es.Close()
os.Remove(dbFile)
}()
testsuite.TestFetcher(t, es, es.All(0))
}
================================================
FILE: eventstore/bbolt/go.mod
================================================
module github.com/hallgren/eventsourcing/eventstore/bbolt
go 1.23
toolchain go1.23.6
require (
github.com/hallgren/eventsourcing/core v0.5.2
go.etcd.io/bbolt v1.4.3
)
require golang.org/x/sys v0.29.0 // indirect
// replace github.com/hallgren/eventsourcing/core => ../../core
================================================
FILE: eventstore/bbolt/go.sum
================================================
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/hallgren/eventsourcing/core v0.5.2 h1:knvM1jP0zziiybce+Au7ysYvZQnDwxkV+/RFZWNDMiw=
github.com/hallgren/eventsourcing/core v0.5.2/go.mod h1:rgo2kFwNVCb0bzUub5nOPlUYNlFkp1uUQBEQx5fM3Lk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: eventstore/bbolt/iterator.go
================================================
package bbolt
import (
"encoding/json"
"errors"
"fmt"
"github.com/hallgren/eventsourcing/core"
"go.etcd.io/bbolt"
)
type Iterator struct {
tx *bbolt.Tx
cursor *bbolt.Cursor
startPosition []byte
value []byte
CurrentGlobalVersion core.Version
}
// Close closes the iterator
func (i *Iterator) Close() {
i.tx.Rollback()
}
func (i *Iterator) Next() bool {
if i.cursor == nil {
return false
}
// first time Next is called go to the start position
if i.value == nil {
_, i.value = i.cursor.Seek(i.startPosition)
} else {
_, i.value = i.cursor.Next()
}
if i.value == nil {
return false
}
return true
}
// Next return the next event
func (i *Iterator) Value() (core.Event, error) {
bEvent := boltEvent{}
err := json.Unmarshal(i.value, &bEvent)
if err != nil {
return core.Event{}, errors.New(fmt.Sprintf("could not deserialize event, %v", err))
}
event := core.Event{
AggregateID: bEvent.AggregateID,
AggregateType: bEvent.AggregateType,
Version: core.Version(bEvent.Version),
GlobalVersion: core.Version(bEvent.GlobalVersion),
Timestamp: bEvent.Timestamp,
Metadata: bEvent.Metadata,
Data: bEvent.Data,
Reason: bEvent.Reason,
}
i.CurrentGlobalVersion = core.Version(bEvent.GlobalVersion)
return event, nil
}
================================================
FILE: eventstore/esdb/README.md
================================================
# esdb event store
The esdb event store is supporting the [EventStoreDB](https://www.eventstore.com) database.
It's based on the module github.com/EventStore/EventStore-Client-Go/v3 for reading and writing events.
================================================
FILE: eventstore/esdb/esdb.go
================================================
package esdb
import (
"context"
"github.com/EventStore/EventStore-Client-Go/v4/esdb"
"github.com/hallgren/eventsourcing/core"
)
const streamSeparator = "-"
// ESDB is the event store handler
type ESDB struct {
client *esdb.Client
contentType esdb.ContentType
}
// Open binds the event store db client
func Open(client *esdb.Client, jsonSerializer bool) *ESDB {
// defaults to binary
var contentType esdb.ContentType
if jsonSerializer {
contentType = esdb.ContentTypeJson
}
return &ESDB{
client: client,
contentType: contentType,
}
}
// Save persists events to the database
func (es *ESDB) Save(events []core.Event) error {
// If no event return no error
if len(events) == 0 {
return nil
}
var streamOptions esdb.AppendToStreamOptions
aggregateID := events[0].AggregateID
aggregateType := events[0].AggregateType
version := events[0].Version
stream := stream(aggregateType, aggregateID)
esdbEvents := make([]esdb.EventData, len(events))
for i, event := range events {
eventData := esdb.EventData{
ContentType: es.contentType,
EventType: event.Reason,
Data: event.Data,
Metadata: event.Metadata,
}
esdbEvents[i] = eventData
}
if version > 1 {
// StreamRevision value -2 due to version in the eventsourcing pkg start on 1 but in esdb on 0
// and also the AppendToStream streamOptions expected revision is one version before the first appended event.
streamOptions.ExpectedRevision = esdb.StreamRevision{Value: uint64(version) - 2}
} else if version == 1 {
streamOptions.ExpectedRevision = esdb.NoStream{}
}
wr, err := es.client.AppendToStream(context.Background(), stream, streamOptions, esdbEvents...)
if err != nil {
if err, ok := esdb.FromError(err); !ok {
if err.Code() == esdb.ErrorCodeWrongExpectedVersion {
// return typed error if version is not the expected.
return core.ErrConcurrency
}
}
return err
}
for i := range events {
// Set all events GlobalVersion to the last events commit position.
events[i].GlobalVersion = core.Version(wr.CommitPosition)
}
return nil
}
func (es *ESDB) Get(ctx context.Context, id string, aggregateType string, afterVersion core.Version) (core.Iterator, error) {
streamID := stream(aggregateType, id)
from := esdb.StreamRevision{Value: uint64(afterVersion)}
stream, err := es.client.ReadStream(ctx, streamID, esdb.ReadStreamOptions{From: from}, ^uint64(0))
if err != nil {
if err, ok := esdb.FromError(err); !ok {
if err.Code() == esdb.ErrorCodeResourceNotFound {
return &Iterator{}, nil
}
}
return nil, err
}
return &Iterator{stream: stream}, nil
}
func stream(aggregateType, aggregateID string) string {
return aggregateType + streamSeparator + aggregateID
}
================================================
FILE: eventstore/esdb/esdb_test.go
================================================
package esdb_test
import (
"context"
"testing"
"github.com/EventStore/EventStore-Client-Go/v4/esdb"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/hallgren/eventsourcing/core"
"github.com/hallgren/eventsourcing/core/testsuite"
es "github.com/hallgren/eventsourcing/eventstore/esdb"
)
func TestSuite(t *testing.T) {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "eventstore/eventstore:latest",
ExposedPorts: []string{"2113/tcp"},
WaitingFor: wait.ForListeningPort("2113/tcp"),
Cmd: []string{"--insecure", "--run-projections=All", "--mem-db"},
}
container, err := testcontainers.GenericContainer(
ctx,
testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
},
)
if err != nil {
t.Fatal(err)
}
defer container.Terminate(ctx)
endpoint, err := container.PortEndpoint(ctx, "2113", "esdb")
if err != nil {
t.Fatal(err)
}
f := func() (core.EventStore, func(), error) {
settings, err := esdb.ParseConnectionString(endpoint + "?tls=false")
if err != nil {
return nil, nil, err
}
db, err := esdb.NewClient(settings)
if err != nil {
return nil, nil, err
}
es := es.Open(db, true)
return es, func() {
}, nil
}
testsuite.Test(t, f)
}
================================================
FILE: eventstore/esdb/go.mod
================================================
module github.com/hallgren/eventsourcing/eventstore/esdb
go 1.25.0
require (
github.com/EventStore/EventStore-Client-Go/v4 v4.2.0
github.com/hallgren/eventsourcing/core v0.5.2
github.com/testcontainers/testcontainers-go v0.42.0
)
require (
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/moby/api v1.54.1 // indirect
github.com/moby/moby/client v0.4.0 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
// replace github.com/hallgren/eventsourcing/core => ../../core
================================================
FILE: eventstore/esdb/go.sum
================================================
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/EventStore/EventStore-Client-Go/v4 v4.2.0 h1:RXKiJ6pGQsWrhZ1BfKwPc1vKh8EOwNSQOXh11whk0Pw=
github.com/EventStore/EventStore-Client-Go/v4 v4.2.0/go.mod h1:KSyk2r/zy2hbkbjHVqBHc0jskYmkNYmXcU5rhMOlWKg=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e h1:XmA6L9IPRdUr28a+SK/oMchGgQy159wvzXA5tJ7l+40=
github.com/goombaio/namegenerator v0.0.0-20181006234301-989e774b106e/go.mod h1:AFIo+02s+12CEg8Gzz9kzhCbmbq6JcKNrhHffCGA9z4=
github.com/hallgren/eventsourcing/core v0.5.2 h1:knvM1jP0zziiybce+Au7ysYvZQnDwxkV+/RFZWNDMiw=
github.com/hallgren/eventsourcing/core v0.5.2/go.mod h1:rgo2kFwNVCb0bzUub5nOPlUYNlFkp1uUQBEQx5fM3Lk=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=
github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=
github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=
github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
================================================
FILE: eventstore/esdb/iterator.go
================================================
package esdb
import (
"strings"
"github.com/EventStore/EventStore-Client-Go/v4/esdb"
"github.com/hallgren/eventsourcing/core"
)
type Iterator struct {
stream *esdb.ReadStream
event *esdb.ResolvedEvent
}
// Close closes the stream
func (i *Iterator) Close() {
i.stream.Close()
}
// Next steps to the next event in the stream
func (i *Iterator) Next() bool {
if i.stream == nil {
return false
}
eventESDB, err := i.stream.Recv()
if err != nil {
return false
}
i.event = eventESDB
return true
}
// Value returns the event from the stream
func (i *Iterator) Value() (core.Event, error) {
stream := strings.Split(i.event.Event.StreamID, streamSeparator)
event := core.Event{
AggregateID: stream[1],
Version: core.Version(i.event.Event.EventNumber) + 1, // +1 as the eventsourcing Version starts on 1 but the esdb event version starts on 0
AggregateType: stream[0],
Timestamp: i.event.Event.CreatedDate,
Data: i.event.Event.Data,
Metadata: i.event.Event.UserMetadata,
Reason: i.event.Event.EventType,
// Can't get the global version when using the ReadStream method
//GlobalVersion: core.Version(event.Event.Position.Commit),
}
return event, nil
}
================================================
FILE: eventstore/kurrent/go.mod
================================================
module github.com/hallgren/eventsourcing/eventstore/kurrent
go 1.25.0
require (
github.com/hallgren/eventsourcing/core v0.5.2
github.com/kurrent-io/KurrentDB-Client-Go v1.1.2
github.com/testcontainers/testcontainers-go v0.42.0
)
require (
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/moby/api v1.54.1 // indirect
github.com/moby/moby/client v0.4.0 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
// replace github.com/hallgren/eventsourcing/core => ../../core
================================================
FILE: eventstore/kurrent/go.sum
================================================
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hallgren/eventsourcing/core v0.5.2 h1:knvM1jP0zziiybce+Au7ysYvZQnDwxkV+/RFZWNDMiw=
github.com/hallgren/eventsourcing/core v0.5.2/go.mod h1:rgo2kFwNVCb0bzUub5nOPlUYNlFkp1uUQBEQx5fM3Lk=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kurrent-io/KurrentDB-Client-Go v1.1.2 h1:Pua/rb8eMbmb4lS3uhf42b6StAVJouA9GD6U611EiM8=
github.com/kurrent-io/KurrentDB-Client-Go v1.1.2/go.mod h1:ddlaMCBJtaKn6gjzjilE4eZ+upG6Aqz/R4tamYTHBTE=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=
github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=
github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=
github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
================================================
FILE: eventstore/kurrent/iterator.go
================================================
package kurrent
import (
"strings"
"github.com/hallgren/eventsourcing/core"
"github.com/kurrent-io/KurrentDB-Client-Go/kurrentdb"
)
type Iterator struct {
Stream *kurrentdb.ReadStream
event *kurrentdb.ResolvedEvent
}
// Close closes the stream
func (i *Iterator) Close() {
i.Stream.Close()
}
// Next steps to the next event in the stream
func (i *Iterator) Next() bool {
if i.Stream == nil {
return false
}
event, err := i.Stream.Recv()
if err != nil {
return false
}
i.event = event
return true
}
// Value returns the event from the stream
func (i *Iterator) Value() (core.Event, error) {
stream := strings.Split(i.event.Event.StreamID, streamSeparator)
event := core.Event{
AggregateID: stream[1],
Version: core.Version(i.event.Event.EventNumber) + 1, // +1 as the eventsourcing Version starts on 1 but the kurrent event version starts on 0
AggregateType: stream[0],
Timestamp: i.event.Event.CreatedDate,
Data: i.event.Event.Data,
Metadata: i.event.Event.UserMetadata,
Reason: i.event.Event.EventType,
// Can't get the global version when using the ReadStream method
//GlobalVersion: core.Version(event.Event.Position.Commit),
}
return event, nil
}
================================================
FILE: eventstore/kurrent/kurrent.go
================================================
package kurrent
import (
"context"
"github.com/hallgren/eventsourcing/core"
"github.com/kurrent-io/KurrentDB-Client-Go/kurrentdb"
)
const streamSeparator = "-"
// Kurrent is the event store handler
type Kurrent struct {
client *kurrentdb.Client
contentType kurrentdb.ContentType
}
// Open binds the event store db client
func Open(client *kurrentdb.Client, jsonSerializer bool) *Kurrent {
// defaults to binary
var contentType kurrentdb.ContentType
if jsonSerializer {
contentType = kurrentdb.ContentTypeJson
}
return &Kurrent{
client: client,
contentType: contentType,
}
}
// Save persists events to the database
func (es *Kurrent) Save(events []core.Event) error {
// If no event return no error
if len(events) == 0 {
return nil
}
var streamOptions kurrentdb.AppendToStreamOptions
aggregateID := events[0].AggregateID
aggregateType := events[0].AggregateType
version := events[0].Version
stream := stream(aggregateType, aggregateID)
KurrentEvents := make([]kurrentdb.EventData, len(events))
for i, event := range events {
eventData := kurrentdb.EventData{
ContentType: es.contentType,
EventType: event.Reason,
Data: event.Data,
Metadata: event.Metadata,
}
KurrentEvents[i] = eventData
}
if version > 1 {
// StreamRevision value -2 due to version in the eventsourcing pkg start on 1 but in kurrent on 0
// and also the AppendToStream streamOptions expected revision is one version before the first appended event.
streamOptions.StreamState = kurrentdb.StreamRevision{Value: uint64(version) - 2}
} else if version == 1 {
streamOptions.StreamState = kurrentdb.NoStream{}
}
wr, err := es.client.AppendToStream(context.Background(), stream, streamOptions, KurrentEvents...)
if err != nil {
if err, ok := kurrentdb.FromError(err); !ok {
if err.Code() == kurrentdb.ErrorCodeWrongExpectedVersion {
// return typed error if version is not the expected.
return core.ErrConcurrency
}
}
return err
}
for i := range events {
// Set all events GlobalVersion to the last events commit position.
events[i].GlobalVersion = core.Version(wr.CommitPosition)
}
return nil
}
func (es *Kurrent) Get(ctx context.Context, id string, aggregateType string, afterVersion core.Version) (core.Iterator, error) {
streamID := stream(aggregateType, id)
from := kurrentdb.StreamRevision{Value: uint64(afterVersion)}
stream, err := es.client.ReadStream(ctx, streamID, kurrentdb.ReadStreamOptions{From: from}, ^uint64(0))
if err != nil {
if err, ok := kurrentdb.FromError(err); !ok {
if err.Code() == kurrentdb.ErrorCodeResourceNotFound {
return &Iterator{}, nil
}
}
return nil, err
}
return &Iterator{Stream: stream}, nil
}
func stream(aggregateType, aggregateID string) string {
return aggregateType + streamSeparator + aggregateID
}
================================================
FILE: eventstore/kurrent/kurrent_test.go
================================================
package kurrent_test
import (
"context"
"testing"
"github.com/kurrent-io/KurrentDB-Client-Go/kurrentdb"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/hallgren/eventsourcing/core"
"github.com/hallgren/eventsourcing/core/testsuite"
"github.com/hallgren/eventsourcing/eventstore/kurrent"
)
func TestSuite(t *testing.T) {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "kurrentplatform/kurrentdb:latest",
ExposedPorts: []string{"2113/tcp"},
WaitingFor: wait.ForListeningPort("2113/tcp"),
Cmd: []string{"--insecure", "--run-projections=All", "--mem-db"},
}
container, err := testcontainers.GenericContainer(
ctx,
testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
},
)
if err != nil {
t.Fatal(err)
}
defer container.Terminate(ctx)
endpoint, err := container.PortEndpoint(ctx, "2113", "kurrent")
if err != nil {
t.Fatal(err)
}
f := func() (core.EventStore, func(), error) {
settings, err := kurrentdb.ParseConnectionString(endpoint + "?tls=false")
if err != nil {
return nil, nil, err
}
db, err := kurrentdb.NewClient(settings)
if err != nil {
return nil, nil, err
}
es := kurrent.Open(db, true)
return es, func() {
}, nil
}
testsuite.Test(t, f)
}
================================================
FILE: eventstore/memory/iterator.go
================================================
package memory
import "github.com/hallgren/eventsourcing/core"
type iterator struct {
events []core.Event
position int
event core.Event
}
func (i *iterator) Next() bool {
if len(i.events) <= i.position {
return false
}
i.event = i.events[i.position]
i.position++
return true
}
func (i *iterator) Value() (core.Event, error) {
return i.event, nil
}
func (i *iterator) Close() {
i.events = nil
i.position = 0
}
================================================
FILE: eventstore/memory/memory.go
================================================
package memory
import (
"context"
"sync"
"github.com/hallgren/eventsourcing/core"
)
// Memory is a handler for event streaming
type Memory struct {
aggregateEvents map[string][]core.Event // The memory structure where we store aggregate events
eventsInOrder []core.Event // The global event order
lock sync.Mutex
}
// Create in memory event store
func Create() *Memory {
return &Memory{
aggregateEvents: make(map[string][]core.Event),
eventsInOrder: make([]core.Event, 0),
}
}
// Save an aggregate (its events)
func (e *Memory) Save(events []core.Event) error {
// Return if there is no events to save
if len(events) == 0 {
return nil
}
// make sure its thread safe
e.lock.Lock()
defer e.lock.Unlock()
// get bucket name from first event
aggregateType := events[0].AggregateType
aggregateID := events[0].AggregateID
bucketName := aggregateKey(aggregateType, aggregateID)
evBucket := e.aggregateEvents[bucketName]
currentVersion := core.Version(0)
if len(evBucket) > 0 {
// Last version in the list
lastEvent := evBucket[len(evBucket)-1]
currentVersion = lastEvent.Version
}
// Make sure no other has saved event to the same aggregate concurrently
if core.Version(currentVersion)+1 != events[0].Version {
return core.ErrConcurrency
}
for i, event := range events {
// set the global version on the event +1 as if the event was already on the eventsInOrder slice
event.GlobalVersion = core.Version(len(e.eventsInOrder) + 1)
evBucket = append(evBucket, event)
e.eventsInOrder = append(e.eventsInOrder, event)
// override the event in the slice exposing the GlobalVersion to the caller
events[i].GlobalVersion = event.GlobalVersion
}
e.aggregateEvents[bucketName] = evBucket
return nil
}
// Get aggregate events
func (e *Memory) Get(ctx context.Context, id string, aggregateType string, afterVersion core.Version) (core.Iterator, error) {
var events []core.Event
// make sure its thread safe
e.lock.Lock()
defer e.lock.Unlock()
for _, e := range e.aggregateEvents[aggregateKey(aggregateType, id)] {
if e.Version > afterVersion {
events = append(events, e)
}
}
return &iterator{events: events}, ctx.Err()
}
// Close does nothing
func (e *Memory) Close() {}
// aggregateKey generates a key to store events against from aggregateType and aggregateID
func aggregateKey(aggregateType, aggregateID string) string {
return aggregateType + "_" + aggregateID
}
// globalEvents returns count events in order globally from the start position
func (e *Memory) globalEvents(start core.Version, count uint64) ([]core.Event, error) {
events := make([]core.Event, 0, count)
// make sure its thread safe
e.lock.Lock()
defer e.lock.Unlock()
for _, e := range e.eventsInOrder {
// find start position and append until counter is 0
if e.GlobalVersion >= start {
events = append(events, e)
count--
if count == 0 {
break
}
}
}
return events, nil
}
// All iterate over all events in GlobalEvents order
func (m *Memory) All(start core.Version, count uint64) core.Fetcher {
return func() (core.Iterator, error) {
events, err := m.globalEvents(start, count)
if err != nil {
return nil, err
}
// no events to fetch
if len(events) == 0 {
return &iterator{events: []core.Event{}}, nil
}
// next time the function is called it will start from the last fetched event +1
start = events[len(events)-1].GlobalVersion + 1
return &iterator{events: events}, nil
}
}
================================================
FILE: eventstore/memory/memory_test.go
================================================
package memory_test
import (
"testing"
"github.com/hallgren/eventsourcing/core"
"github.com/hallgren/eventsourcing/core/testsuite"
"github.com/hallgren/eventsourcing/eventstore/memory"
)
func TestEventStore(t *testing.T) {
f := func() (core.EventStore, func(), error) {
es := memory.Create()
return es, func() { es.Close() }, nil
}
testsuite.Test(t, f)
}
func TestFetcherAll(t *testing.T) {
es := memory.Create()
defer es.Close()
testsuite.TestFetcher(t, es, es.All(0, 10))
}
================================================
FILE: eventstore/sql/README.md
================================================
# SQL Event Store
The sql eventstore is a module containing multiple sql based event stores that are all based on the
database/sql interface in go standard library. The different event stores has specific database schemas
that support the different databases.
## SQLite
Supports the SQLite database https://www.sqlite.org/
### Database Schema
```go
CREATE TABLE IF NOT EXISTS events (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
id VARCHAR NOT NULL,
version INTEGER,
reason VARCHAR,
type VARCHAR,
timestamp VARCHAR,
data BLOB,
metadata BLOB,
UNIQUE (id, type, version)
);
CREATE INDEX IF NOT EXISTS id_type ON events (id, type);
```
### Constructor
```go
// NewSQLite connection to database
NewSQLite(db *sql.DB) (*SQLite, error)
// NewSQLiteSingelWriter prevents multiple writers to save events concurrently
//
// Multiple go routines writing concurrently to sqlite could produce sqlite to lock.
// https://www.sqlite.org/cgi/src/doc/begin-concurrent/doc/begin_concurrent.md
//
// "If there is significant contention for the writer lock, this mechanism can
// be inefficient. In this case it is better for the application to use a mutex
// or some other mechanism that supports blocking to ensure that at most one
// writer is attempting to COMMIT a BEGIN CONCURRENT transaction at a time.
// This is usually easier if all writers are part of the same operating system process."
NewSQLiteSingelWriter(db *sql.DB) (*SQLite, error)
```
### Example of use
```go
import (
// have to alias the sql package as it use the same name
gosql "database/sql"
"github.com/hallgren/eventsourcing/eventstore/sql"
// use the sqlite driver from mattn in this example
_ "github.com/mattn/go-sqlite3"
)
db, err := gosql.Open("sqlite3", "file::memory:?cache=shared")
if err != nil {
return nil, nil, errors.New(fmt.Sprintf("could not open database %v", err))
}
err = db.Ping()
if err != nil {
return nil, nil, errors.New(fmt.Sprintf("could not ping database %v", err))
}
sqliteEventStore, err := sql.NewSQLiteSingelWriter(db)
if err != nil {
return nil, nil, err
}
```
## Postgres
Supports the Postgres database https://www.postgresql.org
### Database Schema
```go
CREATE TABLE IF NOT EXISTS events (
seq SERIAL PRIMARY KEY,
id VARCHAR NOT NULL,
version INTEGER,
reason VARCHAR,
type VARCHAR,
timestamp VARCHAR,
data BYTEA,
metadata BYTEA,
UNIQUE (id, type, version)
);
CREATE INDEX IF NOT EXISTS id_type ON events (id, type);
```
### Constructor
```go
// NewPostgres connection to database
func NewPostgres(db *sql.DB) (*Postgres, error) {
```
### Example of use
```go
import (
// have to alias the sql package as it use the same name
gosql "database/sql"
// in this example we use the pg postgres driver
_ "github.com/lib/pq"
"github.com/hallgren/eventsourcing/eventstore/sql"
)
db, err := gosql.Open("postgres", dsn)
if err != nil {
return nil, nil, fmt.Errorf("db open failed: %w", err)
}
// Test the connection
err = db.Ping()
if err != nil {
return nil, nil, err
}
postgresEventStore, err := sql.NewPostgres(db)
```
## Microsoft SQL Server
Supports Microsoft SQL Server database https://www.microsoft.com/en-us/sql-server
### Database Schema
```go
IF OBJECT_ID('[events]', 'U') IS NULL
BEGIN
CREATE TABLE [events] (
[seq] INT IDENTITY(1,1) PRIMARY KEY,
[id] NVARCHAR(255) NOT NULL,
[version] INT,
[reason] NVARCHAR(255),
[type] NVARCHAR(255),
[timestamp] NVARCHAR(255),
[data] VARBINARY(MAX),
[metadata] VARBINARY(MAX),
CONSTRAINT uq_events UNIQUE ([id], [type], [version])
);
END
IF NOT EXISTS (
SELECT 1
FROM sys.indexes
WHERE name = 'id_type' AND object_id = OBJECT_ID('events')
)
BEGIN
CREATE INDEX id_type ON [events] ([id], [type]);
END
IF NOT EXISTS (
SELECT 1
FROM sys.indexes
WHERE name = 'id_type' AND object_id = OBJECT_ID('events')
)
BEGIN
CREATE INDEX id_type ON [events] ([id], [type]);
END
```
### Constructor
```go
// NewSQLServer connection to database
func NewSQLServer(db *sql.DB) (*SQLServer, error) {
```
### Example of use
```go
import (
// alias the go sql package
gosql "database/sql"
// uses the sql server driver from denisenkom
_ "github.com/denisenkom/go-mssqldb"
"github.com/hallgren/eventsourcing/eventstore/sql"
)
db, err := gosql.Open("sqlserver", dsn)
if err != nil {
return err
}
err = db.Ping() {
if err != nil {
return err
}
SqlServerEventStore, err := sql.NewSQLServer(db)
```
================================================
FILE: eventstore/sql/go.mod
================================================
module github.com/hallgren/eventsourcing/eventstore/sql
go 1.25.0
require (
github.com/denisenkom/go-mssqldb v0.12.3
github.com/hallgren/eventsourcing/core v0.5.2
github.com/lib/pq v1.12.3
github.com/mattn/go-sqlite3 v1.14.42
github.com/testcontainers/testcontainers-go v0.42.0
)
require (
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/moby/api v1.54.1 // indirect
github.com/moby/moby/client v0.4.0 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sys v0.42.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
// replace github.com/hallgren/eventsourcing/core => ../../core
================================================
FILE: eventstore/sql/go.sum
================================================
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hallgren/eventsourcing/core v0.5.2 h1:knvM1jP0zziiybce+Au7ysYvZQnDwxkV+/RFZWNDMiw=
github.com/hallgren/eventsourcing/core v0.5.2/go.mod h1:rgo2kFwNVCb0bzUub5nOPlUYNlFkp1uUQBEQx5fM3Lk=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=
github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=
github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=
github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
================================================
FILE: eventstore/sql/iterator.go
================================================
package sql
import (
"database/sql"
"time"
"github.com/hallgren/eventsourcing/core"
)
type Iterator struct {
Rows *sql.Rows
CurrentGlobalVersion core.Version
}
// Next return true if there are more data
func (i *Iterator) Next() bool {
return i.Rows.Next()
}
// Value return the an event
func (i *Iterator) Value() (core.Event, error) {
var globalVersion core.Version
var version core.Version
var id, reason, typ, timestamp string
var data, metadata []byte
if err := i.Rows.Scan(&globalVersion, &id, &version, &reason, &typ, ×tamp, &data, &metadata); err != nil {
return core.Event{}, err
}
t, err := time.Parse(time.RFC3339, timestamp)
if err != nil {
return core.Event{}, err
}
event := core.Event{
AggregateID: id,
Version: version,
GlobalVersion: globalVersion,
AggregateType: typ,
Timestamp: t,
Data: data,
Metadata: metadata,
Reason: reason,
}
i.CurrentGlobalVersion = globalVersion
return event, nil
}
// Close closes the iterator
func (i *Iterator) Close() {
i.Rows.Close()
}
================================================
FILE: eventstore/sql/migrate.go
================================================
package sql
import (
"context"
"database/sql"
)
func migrate(db *sql.DB, stm []string) error {
tx, err := db.BeginTx(context.Background(), nil)
if err != nil {
return err
}
defer tx.Rollback()
for _, b := range stm {
_, err := tx.Exec(b)
if err != nil {
return err
}
}
return tx.Commit()
}
================================================
FILE: eventstore/sql/postgres.go
================================================
package sql
import (
"context"
"database/sql"
"errors"
"fmt"
"sync"
"time"
"github.com/hallgren/eventsourcing/core"
)
var postgresStm = []string{`CREATE TABLE IF NOT EXISTS events (
seq SERIAL PRIMARY KEY,
id VARCHAR NOT NULL,
version INTEGER,
reason VARCHAR,
type VARCHAR,
timestamp VARCHAR,
data BYTEA,
metadata BYTEA,
UNIQUE (id, type, version)
);`,
`CREATE INDEX IF NOT EXISTS id_type ON events (id, type);`,
}
// Postgres event store handler
type Postgres struct {
db *sql.DB
lock *sync.Mutex
}
// NewPostgres connection to database
func NewPostgres(db *sql.DB) (*Postgres, error) {
// make sure the schema is migrated
if err := migrate(db, postgresStm); err != nil {
return nil, err
}
s := &Postgres{
db: db,
}
return s, nil
}
// Close the connection
func (s *Postgres) Close() {
s.db.Close()
}
// Save persists events to the database
func (s *Postgres) Save(events []core.Event) error {
// If no event return no error
if len(events) == 0 {
return nil
}
if s.lock != nil {
// prevent multiple writers
s.lock.Lock()
defer s.lock.Unlock()
}
aggregateID := events[0].AggregateID
aggregateType := events[0].AggregateType
tx, err := s.db.BeginTx(context.Background(), nil)
if err != nil {
return errors.New(fmt.Sprintf("could not start a write transaction, %v", err))
}
defer tx.Rollback()
var currentVersion core.Version
var version int
selectStm := `SELECT version FROM events WHERE id=$1 and type=$2 ORDER BY version DESC LIMIT 1`
err = tx.QueryRow(selectStm, aggregateID, aggregateType).Scan(&version)
if err != nil && err != sql.ErrNoRows {
return err
} else if err == sql.ErrNoRows {
// if no events are saved before set the current version to zero
currentVersion = core.Version(0)
} else {
// set the current version to the last event stored
currentVersion = core.Version(version)
}
// Make sure no other has saved event to the same aggregate concurrently
if core.Version(currentVersion)+1 != events[0].Version {
return core.ErrConcurrency
}
var lastInsertedID int64
insert := `INSERT INTO events (id, version, reason, type, timestamp, data, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING seq`
for i, event := range events {
err := tx.QueryRow(insert, event.AggregateID, event.Version, event.Reason, event.AggregateType, event.Timestamp.Format(time.RFC3339), event.Data, event.Metadata).Scan(&lastInsertedID)
if err != nil {
return err
}
// override the event in the slice exposing the GlobalVersion to the caller
events[i].GlobalVersion = core.Version(lastInsertedID)
}
return tx.Commit()
}
// Get the events from database
func (s *Postgres) Get(ctx context.Context, id string, aggregateType string, afterVersion core.Version) (core.Iterator, error) {
selectStm := `SELECT seq, id, version, reason, type, timestamp, data, metadata FROM events WHERE id=$1 AND type=$2 AND version>$3 ORDER BY version ASC`
rows, err := s.db.QueryContext(ctx, selectStm, id, aggregateType, afterVersion)
if err != nil {
return nil, err
}
return &Iterator{Rows: rows}, nil
}
// All iterate over all event in GlobalEvents order
func (s *Postgres) All(start core.Version) core.Fetcher {
iter := Iterator{}
return func() (core.Iterator, error) {
// set start from second call and forward
if iter.CurrentGlobalVersion != 0 {
start = iter.CurrentGlobalVersion + 1
}
selectStm := `SELECT seq, id, version, reason, type, timestamp, data, metadata FROM events WHERE seq >= $1 ORDER BY seq ASC`
rows, err := s.db.Query(selectStm, start)
if err != nil {
return nil, err
}
iter.Rows = rows
return &iter, nil
}
}
================================================
FILE: eventstore/sql/postgres_test.go
================================================
package sql_test
import (
"context"
gosql "database/sql"
"fmt"
"testing"
"time"
_ "github.com/lib/pq"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/hallgren/eventsourcing/core"
"github.com/hallgren/eventsourcing/core/testsuite"
"github.com/hallgren/eventsourcing/eventstore/sql"
)
func TestSuitePostgres(t *testing.T) {
dsn, closer, err := postgresServer()
if err != nil {
t.Fatal(err)
}
defer closer()
f := func() (core.EventStore, func(), error) {
es, err := postgreConnect(dsn)
if err != nil {
return nil, nil, err
}
return es, es.Close, nil
}
testsuite.Test(t, f)
}
func TestFetcherAllPostgres(t *testing.T) {
dsn, closer, err := postgresServer()
if err != nil {
t.Fatal(err)
}
defer closer()
es, err := postgreConnect(dsn)
if err != nil {
t.Fatal(err)
}
defer es.Close()
testsuite.TestFetcher(t, es, es.All(0))
}
func postgresServer() (string, func(), error) {
ctx := context.Background()
// Set up the PostgreSQL container request
req := testcontainers.ContainerRequest{
Image: "postgres:16", // Use a specific version
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_USER": "test",
"POSTGRES_PASSWORD": "secret",
"POSTGRES_DB": "testdb",
},
WaitingFor: wait.ForListeningPort("5432/tcp").WithStartupTimeout(30 * time.Second),
}
// Start the container
postgresContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return "", nil, err
}
// Get container host and port
host, _ := postgresContainer.Host(ctx)
port, _ := postgresContainer.MappedPort(ctx, "5432")
// Build the DSN
dsn := fmt.Sprintf("host=%s port=%s user=test password=secret dbname=testdb sslmode=disable", host, port.Port())
return dsn, func() { postgresContainer.Terminate(ctx) }, nil
}
func postgreConnect(dsn string) (*sql.Postgres, error) {
// Connect using database/sql
db, err := gosql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("db open failed: %w", err)
}
// Test the connection
err = db.Ping()
if err != nil {
return nil, err
}
return sql.NewPostgres(db)
}
================================================
FILE: eventstore/sql/sqlite.go
================================================
package sql
import (
"context"
"database/sql"
"errors"
"fmt"
"sync"
"time"
"github.com/hallgren/eventsourcing/core"
)
var stm = []string{
`CREATE TABLE IF NOT EXISTS events (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
id VARCHAR NOT NULL,
version INTEGER,
reason VARCHAR,
type VARCHAR,
timestamp VARCHAR,
data BLOB,
metadata BLOB,
UNIQUE (id, type, version)
);`,
`CREATE INDEX IF NOT EXISTS id_type ON events (id, type);`,
}
// SQLite event store handler
type SQLite struct {
db *sql.DB
lock *sync.Mutex
}
// NewSQLite connection to database
func NewSQLite(db *sql.DB) (*SQLite, error) {
if err := migrate(db, stm); err != nil {
return nil, err
}
return &SQLite{
db: db,
}, nil
}
// NewSQLiteSingelWriter prevents multiple writers to save events concurrently
//
// Multiple go routines writing concurrently to sqlite could produce sqlite to lock.
// https://www.sqlite.org/cgi/src/doc/begin-concurrent/doc/begin_concurrent.md
//
// "If there is significant contention for the writer lock, this mechanism can
// be inefficient. In this case it is better for the application to use a mutex
// or some other mechanism that supports blocking to ensure that at most one
// writer is attempting to COMMIT a BEGIN CONCURRENT transaction at a time.
// This is usually easier if all writers are part of the same operating system process."
func NewSQLiteSingelWriter(db *sql.DB) (*SQLite, error) {
if err := migrate(db, stm); err != nil {
return nil, err
}
return &SQLite{
db: db,
lock: &sync.Mutex{},
}, nil
}
// Close the connection
func (s *SQLite) Close() {
s.db.Close()
}
// Save persists events to the database
func (s *SQLite) Save(events []core.Event) error {
// If no event return no error
if len(events) == 0 {
return nil
}
if s.lock != nil {
// prevent multiple writers
s.lock.Lock()
defer s.lock.Unlock()
}
aggregateID := events[0].AggregateID
aggregateType := events[0].AggregateType
tx, err := s.db.BeginTx(context.Background(), nil)
if err != nil {
return errors.New(fmt.Sprintf("could not start a write transaction, %v", err))
}
defer tx.Rollback()
var currentVersion core.Version
var version int
selectStm := `Select version from events where id=? and type=? order by version desc limit 1`
err = tx.QueryRow(selectStm, aggregateID, aggregateType).Scan(&version)
if err != nil && err != sql.ErrNoRows {
return err
} else if err == sql.ErrNoRows {
// if no events are saved before set the current version to zero
currentVersion = core.Version(0)
} else {
// set the current version to the last event stored
currentVersion = core.Version(version)
}
// Make sure no other has saved event to the same aggregate concurrently
if core.Version(currentVersion)+1 != events[0].Version {
return core.ErrConcurrency
}
var lastInsertedID int64
insert := `Insert into events (id, version, reason, type, timestamp, data, metadata) values ($1, $2, $3, $4, $5, $6, $7)`
for i, event := range events {
res, err := tx.Exec(insert, event.AggregateID, event.Version, event.Reason, event.AggregateType, event.Timestamp.Format(time.RFC3339), event.Data, event.Metadata)
if err != nil {
return err
}
lastInsertedID, err = res.LastInsertId()
if err != nil {
return err
}
// override the event in the slice exposing the GlobalVersion to the caller
events[i].GlobalVersion = core.Version(lastInsertedID)
}
return tx.Commit()
}
// Get the events from database
func (s *SQLite) Get(ctx context.Context, id string, aggregateType string, afterVersion core.Version) (core.Iterator, error) {
selectStm := `Select seq, id, version, reason, type, timestamp, data, metadata from events where id=? and type=? and version>? order by version asc`
rows, err := s.db.QueryContext(ctx, selectStm, id, aggregateType, afterVersion)
if err != nil {
return nil, err
}
return &Iterator{Rows: rows}, nil
}
// All iterate over all event in GlobalEvents order
func (s *SQLite) All(start core.Version) core.Fetcher {
iter := Iterator{}
return func() (core.Iterator, error) {
// set start from second call and forward
if iter.CurrentGlobalVersion != 0 {
start = iter.CurrentGlobalVersion + 1
}
selectStm := `Select seq, id, version, reason, type, timestamp, data, metadata from events where seq >= ? order by seq asc`
rows, err := s.db.Query(selectStm, start)
if err != nil {
return nil, err
}
iter.Rows = rows
return &iter, nil
}
}
================================================
FILE: eventstore/sql/sqlite_test.go
================================================
package sql_test
import (
sqldriver "database/sql"
"errors"
"fmt"
"testing"
"github.com/hallgren/eventsourcing/core"
"github.com/hallgren/eventsourcing/core/testsuite"
"github.com/hallgren/eventsourcing/eventstore/sql"
_ "github.com/mattn/go-sqlite3"
)
func TestSuiteSQLite(t *testing.T) {
f := func() (core.EventStore, func(), error) {
return eventstore(false)
}
testsuite.Test(t, f)
}
func TestSuiteSQLiteSingelWriter(t *testing.T) {
f := func() (core.EventStore, func(), error) {
return eventstore(true)
}
testsuite.Test(t, f)
}
func TestFetchFuncAll(t *testing.T) {
es, close, err := eventstore(false)
if err != nil {
t.Fatal(err)
}
defer close()
testsuite.TestFetcher(t, es, es.All(0))
}
func eventstore(singelWriter bool) (*sql.SQLite, func(), error) {
var es *sql.SQLite
db, err := sqldriver.Open("sqlite3", "file::memory:?cache=shared")
if err != nil {
return nil, nil, errors.New(fmt.Sprintf("could not open database %v", err))
}
err = db.Ping()
if err != nil {
return nil, nil, errors.New(fmt.Sprintf("could not ping database %v", err))
}
if singelWriter {
es, err = sql.NewSQLiteSingelWriter(db)
if err != nil {
return nil, nil, err
}
} else {
// to make the concurrent test pass (not have to use this in the sql.OpenWithSingelWriter constructor)
db.SetMaxOpenConns(1)
es, err = sql.NewSQLite(db)
if err != nil {
return nil, nil, err
}
}
return es, func() {
es.Close()
}, nil
}
================================================
FILE: eventstore/sql/sqlserver.go
================================================
package sql
import (
"context"
"database/sql"
"errors"
"fmt"
"sync"
"time"
"github.com/hallgren/eventsourcing/core"
)
const createTableSQLServer = `IF OBJECT_ID('[events]', 'U') IS NULL
BEGIN
CREATE TABLE [events] (
[seq] INT IDENTITY(1,1) PRIMARY KEY,
[id] NVARCHAR(255) NOT NULL,
[version] INT,
[reason] NVARCHAR(255),
[type] NVARCHAR(255),
[timestamp] NVARCHAR(255),
[data] VARBINARY(MAX),
[metadata] VARBINARY(MAX),
CONSTRAINT uq_events UNIQUE ([id], [type], [version])
);
END`
const indexSQLServer = `IF NOT EXISTS (
SELECT 1
FROM sys.indexes
WHERE name = 'id_type' AND object_id = OBJECT_ID('events')
)
BEGIN
CREATE INDEX id_type ON [events] ([id], [type]);
END`
var stmSQLServer = []string{
createTableSQLServer,
indexSQLServer,
}
// SQLServer event store handler
type SQLServer struct {
db *sql.DB
lock *sync.Mutex
}
// NewSQLServer connection to database
func NewSQLServer(db *sql.DB) (*SQLServer, error) {
if err := migrate(db, stmSQLServer); err != nil {
return nil, err
}
return &SQLServer{
db: db,
}, nil
}
// Close the connection
func (s *SQLServer) Close() {
s.db.Close()
}
// Save persists events to the database
func (s *SQLServer) Save(events []core.Event) error {
// If no event return no error
if len(events) == 0 {
return nil
}
if s.lock != nil {
// prevent multiple writers
s.lock.Lock()
defer s.lock.Unlock()
}
aggregateID := events[0].AggregateID
aggregateType := events[0].AggregateType
tx, err := s.db.BeginTx(context.Background(), nil)
if err != nil {
return errors.New(fmt.Sprintf("could not start a write transaction, %v", err))
}
defer tx.Rollback()
var currentVersion core.Version
var version int
selectStm := `SELECT TOP 1 version FROM [events] WHERE [id] = @id AND [type] = @type ORDER BY version DESC;`
err = tx.QueryRow(selectStm, sql.Named("id", aggregateID), sql.Named("type", aggregateType)).Scan(&version)
if err != nil && err != sql.ErrNoRows {
return err
} else if err == sql.ErrNoRows {
// if no events are saved before set the current version to zero
currentVersion = core.Version(0)
} else {
// set the current version to the last event stored
currentVersion = core.Version(version)
}
// Make sure no other has saved event to the same aggregate concurrently
if core.Version(currentVersion)+1 != events[0].Version {
return core.ErrConcurrency
}
var lastInsertedID int64
insert := `INSERT INTO [events] (id, version, reason, type, timestamp, data, metadata)
OUTPUT INSERTED.seq
VALUES (@id, @version, @reason, @type, @timestamp, @data, @metadata);`
for i, event := range events {
err := tx.QueryRow(
insert,
sql.Named("id", event.AggregateID),
sql.Named("version", event.Version),
sql.Named("reason", event.Reason),
sql.Named("type", event.AggregateType),
sql.Named("timestamp", event.Timestamp.Format(time.RFC3339)),
sql.Named("data", event.Data),
sql.Named("metadata", event.Metadata),
).Scan(&lastInsertedID)
if err != nil {
return err
}
// override the event in the slice exposing the GlobalVersion to the caller
events[i].GlobalVersion = core.Version(lastInsertedID)
}
return tx.Commit()
}
// Get the events from database
func (s *SQLServer) Get(ctx context.Context, id string, aggregateType string, afterVersion core.Version) (core.Iterator, error) {
selectStm := `SELECT seq, id, version, reason, type, timestamp, data, metadata
FROM [events]
WHERE id = @id AND type = @type AND version > @version
ORDER BY version ASC;`
rows, err := s.db.QueryContext(ctx, selectStm, sql.Named("id", id), sql.Named("type", aggregateType), sql.Named("version", afterVersion))
if err != nil {
return nil, err
}
return &Iterator{Rows: rows}, nil
}
// All iterate over all event in GlobalEvents order
func (s *SQLServer) All(start core.Version) core.Fetcher {
iter := Iterator{}
return func() (core.Iterator, error) {
// set start from second call and forward
if iter.CurrentGlobalVersion != 0 {
start = iter.CurrentGlobalVersion + 1
}
selectStm := `SELECT seq, id, version, reason, type, timestamp, data, metadata
FROM [events]
WHERE seq >= @start
ORDER BY seq ASC;`
rows, err := s.db.Query(selectStm, sql.Named("start", start))
if err != nil {
return nil, err
}
iter.Rows = rows
return &iter, nil
}
}
================================================
FILE: eventstore/sql/sqlserver_test.go
================================================
package sql_test
import (
"context"
gosql "database/sql"
"fmt"
"testing"
"time"
_ "github.com/denisenkom/go-mssqldb"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/hallgren/eventsourcing/core"
"github.com/hallgren/eventsourcing/core/testsuite"
"github.com/hallgren/eventsourcing/eventstore/sql"
)
func TestSuiteSQLServer(t *testing.T) {
dsn, closer, err := sqlServer()
if err != nil {
t.Fatal(err)
}
defer closer()
f := func() (core.EventStore, func(), error) {
es, err := sqlServerConnect(dsn)
if err != nil {
return nil, nil, err
}
return es, es.Close, nil
}
testsuite.Test(t, f)
}
func TestFetcherAllSQLServer(t *testing.T) {
dsn, closer, err := sqlServer()
if err != nil {
t.Fatal(err)
}
defer closer()
es, err := sqlServerConnect(dsn)
if err != nil {
t.Fatal(err)
}
defer es.Close()
testsuite.TestFetcher(t, es, es.All(0))
}
func sqlServerConnect(dsn string) (*sql.SQLServer, error) {
var db *gosql.DB
var err error
for i := 0; i < 10; i++ {
// Connect using database/sql
db, err = gosql.Open("sqlserver", dsn)
// Test the connection
if err == nil && db.Ping() == nil {
break
}
time.Sleep(2 * time.Second)
}
if err != nil {
return nil, err
}
return sql.NewSQLServer(db)
}
func sqlServer() (string, func(), error) {
ctx := context.Background()
// Start MSSQL container
req := testcontainers.ContainerRequest{
Image: "mcr.microsoft.com/mssql/server:2019-latest",
ExposedPorts: []string{"1433/tcp"},
Env: map[string]string{
"ACCEPT_EULA": "Y",
"SA_PASSWORD": "YourStrong(!)Password",
},
WaitingFor: wait.ForLog("SQL Server is now ready for client connections").WithStartupTimeout(2 * time.Minute),
}
mssqlC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return "", nil, err
}
host, err := mssqlC.Host(ctx)
if err != nil {
return "", nil, err
}
port, err := mssqlC.MappedPort(ctx, "1433")
if err != nil {
return "", nil, err
}
dsn := fmt.Sprintf("sqlserver://sa:YourStrong(!)Password@%s:%s?database=master", host, port.Port())
return dsn, func() { mssqlC.Terminate(ctx) }, nil
}
================================================
FILE: example/README.md
================================================
## Collection of reference projects
* [Ardan labs](https://github.com/hallgren/ardanlabs) - Example when presenting event sourcing for Ardan Labs.
* [Internal company presentation](https://github.com/hallgren/kundskapsspridning) - Used in company presentation.
* [event-sourcing-tech-talk](https://github.com/kieranajp/event-sourcing-tech-talk) - Made by [kieranajp](https://github.com/kieranajp)
================================================
FILE: example/go.mod
================================================
module github.com/hallgren/eventsourcing/example
go 1.22
require github.com/hallgren/eventsourcing v0.8.1
require github.com/hallgren/eventsourcing/core v0.4.0 // indirect
// replace github.com/hallgren/eventsourcing => ../.
================================================
FILE: example/go.sum
================================================
github.com/hallgren/eventsourcing v0.8.0 h1:lFMWRdc59+tRwzCzmby8I3qr2WbD1GWqB/ypJrBMbVc=
github.com/hallgren/eventsourcing v0.8.0/go.mod h1:62QwV0m8wLQM6SZwtuGn4lwA5UUFozFnALyZvFeX9ps=
github.com/hallgren/eventsourcing v0.8.1 h1:SHefx3Wf2K1ylmGBzykSmt4dRVnDTQGYjODiM69c4uU=
github.com/hallgren/eventsourcing v0.8.1/go.mod h1:62QwV0m8wLQM6SZwtuGn4lwA5UUFozFnALyZvFeX9ps=
github.com/hallgren/eventsourcing/core v0.4.0 h1:a11TT3df7JlrZtIogqbGmLGgmeugRavwD8HrLtW1Uxw=
github.com/hallgren/eventsourcing/core v0.4.0/go.mod h1:rgo2kFwNVCb0bzUub5nOPlUYNlFkp1uUQBEQx5fM3Lk=
================================================
FILE: example/order/cmd/main.go
================================================
package main
import (
"context"
"fmt"
"time"
"github.com/hallgren/eventsourcing"
"github.com/hallgren/eventsourcing/aggregate"
"github.com/hallgren/eventsourcing/eventstore/memory"
"github.com/hallgren/eventsourcing/example/order"
)
type Order struct {
DiscountAmount uint
Total uint
}
func main() {
es := memory.Create()
aggregate.Register(&order.Order{})
ongoingOrders := make(map[string]*Order)
completedCount := 0
moneyMade := 0
totalDiscountAmount := 0
go func() {
i := 0
for {
o, err := order.Create(100)
if err != nil {
panic(err)
}
err = o.AddDiscount(10)
if err != nil {
panic(err)
}
// 33% of orders remove discount
if i%3 == 0 {
o.RemoveDiscount()
}
err = o.Pay(80)
if err != nil {
panic(err)
}
// 25% of orders are not completed
if i%4 == 0 {
err = o.Pay(o.Outstanding)
if err != nil {
panic(err)
}
}
err = aggregate.Save(es, o)
if err != nil {
panic(err)
}
time.Sleep(time.Second)
i++
}
}()
for {
// setup how the projection will handle events and build the read model
p := eventsourcing.NewProjection(es.All(0, 3), func(e eventsourcing.Event) error {
switch event := e.Data().(type) {
// When an order is created add it to an order map
case *order.Created:
ongoingOrders[e.AggregateID()] = &Order{
Total: event.Total,
}
// When order is complete add the discount amount to a sum also store the total amount of completed orders
case *order.Completed:
completedCount++
totalDiscountAmount += int(ongoingOrders[e.AggregateID()].DiscountAmount)
// delete the order from the map
delete(ongoingOrders, e.AggregateID())
// Store the discount amount in the order in the map
case *order.DiscountApplied:
ongoingOrders[e.AggregateID()].DiscountAmount = ongoingOrders[e.AggregateID()].Total - event.Total
// If the discount is removed reset the discount amount in the orders map
case *order.DiscountRemoved:
ongoingOrders[e.AggregateID()].DiscountAmount = 0
// If a payment is made store the value
case *order.Paid:
moneyMade += int(event.Amount)
}
fmt.Println("active order:", len(ongoingOrders), "completed orders:", completedCount, "money made:", moneyMade, "total discount amount:", totalDiscountAmount)
return nil
})
p.Run(context.Background(), time.Second*2)
}
}
================================================
FILE: example/order/order.go
================================================
package order
import (
"fmt"
"github.com/hallgren/eventsourcing"
"github.com/hallgren/eventsourcing/aggregate"
)
type Status string
const (
Pending Status = "pending"
Complete Status = "complete"
)
// Aggregate
// Collects the business rules for the Order
//
// Rules
// 1. Multiple discounts is not allowed
// 2. Order amount can't be above 500
// 3. Completed order can't be altered
// 4. If a payment has been made it's not possible to alter the discount.
// Order is the aggregate protecting the state
type Order struct {
aggregate.Root
Status Status
Total uint
Discount uint
Outstanding uint
Paid uint
}
// Transition builds the aggregate state based on the events
func (o *Order) Transition(event eventsourcing.Event) {
switch e := event.Data().(type) {
case *Created:
o.Status = Pending
o.Total = e.Total
o.Outstanding = e.Total
case *DiscountApplied:
o.Discount = e.Percentage
o.Outstanding = e.Total
case *DiscountRemoved:
o.Discount = 0
o.Outstanding = o.Total
case *Paid:
o.Outstanding -= e.Amount
o.Paid += e.Amount
case *Completed:
o.Status = Complete
}
}
// Events
// Defines all possible events for the Order aggregate
// Register is a eventsouring helper function that must be defined on
// the aggregate.
func (o *Order) Register(r aggregate.RegisterFunc) {
r(
&Created{},
&DiscountApplied{},
&DiscountRemoved{},
&Paid{},
&Completed{},
)
}
// Created when the order was created
type Created struct {
Total uint
}
// DiscountApplied when a discount was applied
type DiscountApplied struct {
Percentage uint
Total uint
}
// DiscountRemoved when the discount was removed
type DiscountRemoved struct{}
// Paid an amount from the total
type Paid struct {
Amount uint
}
// Completed - the order is fully paid
type Completed struct{}
// Commands
// Holds the business logic and protects the aggregate (Order) state.
// Events should only be created via commands.
// Create creates the initial order
func Create(amount uint) (*Order, error) {
if amount > 500 {
return nil, fmt.Errorf("amount can't be higher than 500")
}
o := Order{}
aggregate.TrackChange(&o, &Created{Total: amount})
return &o, nil
}
// AddDiscount adds discount to the order
func (o *Order) AddDiscount(percentage uint) error {
if o.Status == Complete {
return fmt.Errorf("can't add discount on completed order")
}
if o.Discount > 0 {
return fmt.Errorf("there is already an active discount")
}
if o.Paid > 0 {
return fmt.Errorf("can't add discount on order with payments")
}
if percentage > 25 {
return fmt.Errorf("discount can't be over 25 was %d", percentage)
}
// ignore if discount percentage is zero
if percentage == 0 {
return nil
}
discountFloat := float64(percentage) / 100.0
newTotal := o.Total - uint(float64(o.Total)*discountFloat)
aggregate.TrackChange(o, &DiscountApplied{Percentage: percentage, Total: newTotal})
return nil
}
// RemoveDiscount removes the discount if any otherwise ignore
func (o *Order) RemoveDiscount() {
// No discount applied
if o.Discount == 0 {
return
}
aggregate.TrackChange(o, &DiscountRemoved{})
return
}
// Pay creates a payment on the order. If the outstanding amount is zero the order
// is paid.
func (o *Order) Pay(amount uint) error {
if
gitextract_nsi1zgi3/
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ └── go.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── aggregate/
│ ├── aggregate.go
│ ├── aggregate_test.go
│ ├── idgenerator.go
│ ├── root.go
│ ├── root_test.go
│ ├── snapshot.go
│ └── snapshot_test.go
├── core/
│ ├── event.go
│ ├── eventstore.go
│ ├── fetcher.go
│ ├── go.mod
│ ├── snapshotstore.go
│ └── testsuite/
│ ├── eventstoresuite.go
│ ├── fetcher.go
│ └── snapshotstoresuite.go
├── event.go
├── eventsourcing.go
├── eventsourcing_test.go
├── eventstore/
│ ├── bbolt/
│ │ ├── README.md
│ │ ├── bbolt.go
│ │ ├── bbolt_test.go
│ │ ├── go.mod
│ │ ├── go.sum
│ │ └── iterator.go
│ ├── esdb/
│ │ ├── README.md
│ │ ├── esdb.go
│ │ ├── esdb_test.go
│ │ ├── go.mod
│ │ ├── go.sum
│ │ └── iterator.go
│ ├── kurrent/
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── iterator.go
│ │ ├── kurrent.go
│ │ └── kurrent_test.go
│ ├── memory/
│ │ ├── iterator.go
│ │ ├── memory.go
│ │ └── memory_test.go
│ └── sql/
│ ├── README.md
│ ├── go.mod
│ ├── go.sum
│ ├── iterator.go
│ ├── migrate.go
│ ├── postgres.go
│ ├── postgres_test.go
│ ├── sqlite.go
│ ├── sqlite_test.go
│ ├── sqlserver.go
│ └── sqlserver_test.go
├── example/
│ ├── README.md
│ ├── go.mod
│ ├── go.sum
│ ├── order/
│ │ ├── cmd/
│ │ │ └── main.go
│ │ ├── order.go
│ │ └── order_test.go
│ └── tictactoe/
│ ├── cmd/
│ │ └── main/
│ │ └── main.go
│ ├── tictactoe.go
│ └── tictactoe_test.go
├── go.mod
├── go.sum
├── internal/
│ ├── encoderjson.go
│ └── register.go
├── iterator.go
├── projections.go
├── projections_test.go
└── snapshotstore/
├── memory/
│ ├── memory.go
│ └── memory_test.go
└── sql/
├── README.md
├── go.mod
├── go.sum
├── migrate.go
├── postgres.go
├── postgres_test.go
├── sqlite.go
└── sqlite_test.go
SYMBOL INDEX (328 symbols across 54 files)
FILE: aggregate/aggregate.go
type aggregate (line 17) | type aggregate interface
function Load (line 24) | func Load(ctx context.Context, es core.EventStore, id string, a aggregat...
function LoadFromSnapshot (line 56) | func LoadFromSnapshot(ctx context.Context, es core.EventStore, ss core.S...
function Save (line 65) | func Save(es core.EventStore, a aggregate) error {
function Register (line 93) | func Register(a aggregate) {
function saveEvents (line 98) | func saveEvents(eventStore core.EventStore, events []eventsourcing.Event...
function getEvents (line 139) | func getEvents(ctx context.Context, eventStore core.EventStore, id, aggr...
FILE: aggregate/aggregate_test.go
function TestSaveAndLoadAggregate (line 13) | func TestSaveAndLoadAggregate(t *testing.T) {
function TestLoadAggregateFromSnapshot (line 48) | func TestLoadAggregateFromSnapshot(t *testing.T) {
function TestLoadNoneExistingAggregate (line 83) | func TestLoadNoneExistingAggregate(t *testing.T) {
FILE: aggregate/idgenerator.go
function SetIDFunc (line 13) | func SetIDFunc(f func() string) {
function randSeq (line 17) | func randSeq() string {
function generateRandomString (line 25) | func generateRandomString(n int) (string, error) {
function generateRandomBytes (line 37) | func generateRandomBytes(n int) ([]byte, error) {
FILE: aggregate/root.go
type Root (line 12) | type Root struct
method nextVersion (line 64) | func (ar *Root) nextVersion() core.Version {
method SetID (line 69) | func (ar *Root) SetID(id string) error {
method ID (line 78) | func (ar *Root) ID() string {
method root (line 85) | func (ar *Root) root() *Root {
method Version (line 90) | func (ar *Root) Version() eventsourcing.Version {
method GlobalVersion (line 98) | func (ar *Root) GlobalVersion() eventsourcing.Version {
method Events (line 106) | func (ar *Root) Events() []eventsourcing.Event {
method UnsavedEvents (line 116) | func (ar *Root) UnsavedEvents() bool {
constant emptyID (line 19) | emptyID = ""
function TrackChange (line 23) | func TrackChange(a aggregate, data interface{}) {
function TrackChangeWithMetadata (line 30) | func TrackChangeWithMetadata(a aggregate, data interface{}, metadata map...
function buildFromHistory (line 52) | func buildFromHistory(a aggregate, events []eventsourcing.Event) {
function aggregateType (line 120) | func aggregateType(a interface{}) string {
FILE: aggregate/root_test.go
type Person (line 15) | type Person struct
method GrowOlder (line 59) | func (person *Person) GrowOlder() {
method Register (line 66) | func (person *Person) Register(f aggregate.RegisterFunc) {
method Transition (line 71) | func (person *Person) Transition(event eventsourcing.Event) {
method SerializeSnapshot (line 81) | func (person *Person) SerializeSnapshot(aggregate.SnapshotMarshal) ([]...
method DeserializeSnapshot (line 84) | func (person *Person) DeserializeSnapshot(f aggregate.SnapshotUnmarsha...
type Born (line 23) | type Born struct
type AgedOneYear (line 28) | type AgedOneYear struct
function CreatePerson (line 32) | func CreatePerson(name string) (*Person, error) {
function CreatePersonWithID (line 42) | func CreatePersonWithID(id, name string) (*Person, error) {
function TestPersonWithNoEvents (line 88) | func TestPersonWithNoEvents(t *testing.T) {
function TestCreateNewPerson (line 95) | func TestCreateNewPerson(t *testing.T) {
function TestCreateNewPersonWithIDFromOutside (line 135) | func TestCreateNewPersonWithIDFromOutside(t *testing.T) {
function TestBlankName (line 147) | func TestBlankName(t *testing.T) {
function TestSetIDOnExistingPerson (line 154) | func TestSetIDOnExistingPerson(t *testing.T) {
function TestPersonAgedOneYear (line 166) | func TestPersonAgedOneYear(t *testing.T) {
function TestPersonGrewTenYears (line 193) | func TestPersonGrewTenYears(t *testing.T) {
function TestSetIDFunc (line 204) | func TestSetIDFunc(t *testing.T) {
function TestIDFuncGeneratingRandomIDs (line 220) | func TestIDFuncGeneratingRandomIDs(t *testing.T) {
FILE: aggregate/snapshot.go
type SnapshotMarshal (line 13) | type SnapshotMarshal
type SnapshotUnmarshal (line 14) | type SnapshotUnmarshal
type snapshot (line 17) | type snapshot interface
type aggregateSnapshot (line 23) | type aggregateSnapshot interface
function LoadSnapshot (line 30) | func LoadSnapshot(ctx context.Context, ss core.SnapshotStore, id string,...
function getSnapshot (line 41) | func getSnapshot(ctx context.Context, ss core.SnapshotStore, id string, ...
function SaveSnapshot (line 62) | func SaveSnapshot(ss core.SnapshotStore, s snapshot) error {
FILE: aggregate/snapshot_test.go
function createPerson (line 15) | func createPerson() *Person {
function TestSaveAndGetSnapshot (line 27) | func TestSaveAndGetSnapshot(t *testing.T) {
function TestGetNoneExistingSnapshotOrEvents (line 55) | func TestGetNoneExistingSnapshotOrEvents(t *testing.T) {
function TestGetNoneExistingSnapshot (line 65) | func TestGetNoneExistingSnapshot(t *testing.T) {
function TestSaveSnapshotWithUnsavedEvents (line 75) | func TestSaveSnapshotWithUnsavedEvents(t *testing.T) {
type snapshot (line 89) | type snapshot struct
method Command (line 110) | func (s *snapshot) Command() {
method Transition (line 115) | func (s *snapshot) Transition(e eventsourcing.Event) {
method Register (line 127) | func (s *snapshot) Register(f aggregate.RegisterFunc) {
method SerializeSnapshot (line 136) | func (s *snapshot) SerializeSnapshot(f aggregate.SnapshotMarshal) ([]b...
method DeserializeSnapshot (line 144) | func (s *snapshot) DeserializeSnapshot(f aggregate.SnapshotUnmarshal, ...
type Event (line 97) | type Event struct
type Event2 (line 98) | type Event2 struct
function New (line 100) | func New() *snapshot {
type snapshotInternal (line 131) | type snapshotInternal struct
function TestSnapshotNoneExported (line 155) | func TestSnapshotNoneExported(t *testing.T) {
FILE: core/event.go
type Version (line 8) | type Version
type Event (line 11) | type Event struct
FILE: core/eventstore.go
type Iterator (line 12) | type Iterator interface
type EventStore (line 19) | type EventStore interface
FILE: core/fetcher.go
type Fetcher (line 4) | type Fetcher
FILE: core/snapshotstore.go
type Snapshot (line 12) | type Snapshot struct
type SnapshotStore (line 21) | type SnapshotStore interface
FILE: core/testsuite/eventstoresuite.go
function AggregateID (line 18) | func AggregateID() string {
type Status (line 26) | type Status
constant StatusRed (line 29) | StatusRed Status = iota
constant StatusSilver (line 30) | StatusSilver Status = iota
constant StatusGold (line 31) | StatusGold Status = iota
type FrequentFlierAccountCreated (line 34) | type FrequentFlierAccountCreated struct
type StatusMatched (line 40) | type StatusMatched struct
type FlightTaken (line 44) | type FlightTaken struct
function eventToByte (line 52) | func eventToByte(i interface{}) []byte {
function testEvents (line 57) | func testEvents(aggregateID string) []core.Event {
function testEventsPartTwo (line 71) | func testEventsPartTwo(aggregateID string) []core.Event {
function testEventOtherAggregate (line 79) | func testEventOtherAggregate(aggregateID string) core.Event {
function Test (line 83) | func Test(t *testing.T, esFunc eventstoreFunc) {
function saveAndGetEvents (line 112) | func saveAndGetEvents(es core.EventStore) error {
function getEventsAfterVersion (line 181) | func getEventsAfterVersion(es core.EventStore) error {
function saveEventsInWrongVersion (line 213) | func saveEventsInWrongVersion(es core.EventStore) error {
function saveAndGetEventsConcurrently (line 224) | func saveAndGetEventsConcurrently(es core.EventStore) error {
function getErrWhenNoEvents (line 277) | func getErrWhenNoEvents(es core.EventStore) error {
function saveReturnGlobalEventOrder (line 290) | func saveReturnGlobalEventOrder(es core.EventStore) error {
FILE: core/testsuite/fetcher.go
function TestFetcher (line 10) | func TestFetcher(t *testing.T, es core.EventStore, fetcher core.Fetcher) {
function verify (line 55) | func verify(iter core.Iterator, globalVersion core.Version, expEvents []...
FILE: core/testsuite/snapshotstoresuite.go
function TestSnapshotStore (line 14) | func TestSnapshotStore(t *testing.T, ssFunc snapshotstoreFunc) {
function saveAndGetSnapshot (line 39) | func saveAndGetSnapshot(ss core.SnapshotStore) error {
function getNoneExistingSnapshot (line 77) | func getNoneExistingSnapshot(ss core.SnapshotStore) error {
FILE: event.go
type Version (line 11) | type Version
type Event (line 13) | type Event struct
method Data (line 23) | func (e Event) Data() interface{} {
method Metadata (line 27) | func (e Event) Metadata() map[string]interface{} {
method AggregateType (line 31) | func (e Event) AggregateType() string {
method AggregateID (line 35) | func (e Event) AggregateID() string {
method Reason (line 39) | func (e Event) Reason() string {
method Version (line 46) | func (e Event) Version() Version {
method Timestamp (line 50) | func (e Event) Timestamp() time.Time {
method GlobalVersion (line 54) | func (e Event) GlobalVersion() Version {
function NewEvent (line 19) | func NewEvent(e core.Event, data interface{}, metadata map[string]interf...
FILE: eventsourcing.go
type Encoder (line 33) | type Encoder interface
function SetEventEncoder (line 39) | func SetEventEncoder(e Encoder) {
function SetSnapshotEncoder (line 44) | func SetSnapshotEncoder(e Encoder) {
FILE: eventstore/bbolt/bbolt.go
constant globalEventOrderBucketName (line 16) | globalEventOrderBucketName = "global_event_order"
type BBolt (line 20) | type BBolt struct
method Save (line 61) | func (e *BBolt) Save(events []core.Event) error {
method Get (line 158) | func (e *BBolt) Get(ctx context.Context, id string, aggregateType stri...
method All (line 172) | func (e *BBolt) All(start core.Version) core.Fetcher {
method Close (line 199) | func (e *BBolt) Close() error {
method createBucket (line 204) | func (e *BBolt) createBucket(bucketRef []byte, tx *bbolt.Tx) error {
type boltEvent (line 24) | type boltEvent struct
function New (line 37) | func New(dbFile string) (*BBolt, error) {
function bucketRef (line 213) | func bucketRef(aggregateType, aggregateID string) []byte {
function position (line 218) | func position(p core.Version) []byte {
function itob (line 223) | func itob(v uint64) []byte {
FILE: eventstore/bbolt/bbolt_test.go
function TestEventStoreSuite (line 12) | func TestEventStoreSuite(t *testing.T) {
function TestFetchFuncAll (line 27) | func TestFetchFuncAll(t *testing.T) {
FILE: eventstore/bbolt/iterator.go
type Iterator (line 12) | type Iterator struct
method Close (line 21) | func (i *Iterator) Close() {
method Next (line 25) | func (i *Iterator) Next() bool {
method Value (line 43) | func (i *Iterator) Value() (core.Event, error) {
FILE: eventstore/esdb/esdb.go
constant streamSeparator (line 10) | streamSeparator = "-"
type ESDB (line 13) | type ESDB struct
method Save (line 32) | func (es *ESDB) Save(events []core.Event) error {
method Get (line 80) | func (es *ESDB) Get(ctx context.Context, id string, aggregateType stri...
function Open (line 19) | func Open(client *esdb.Client, jsonSerializer bool) *ESDB {
function stream (line 96) | func stream(aggregateType, aggregateID string) string {
FILE: eventstore/esdb/esdb_test.go
function TestSuite (line 16) | func TestSuite(t *testing.T) {
FILE: eventstore/esdb/iterator.go
type Iterator (line 10) | type Iterator struct
method Close (line 16) | func (i *Iterator) Close() {
method Next (line 21) | func (i *Iterator) Next() bool {
method Value (line 34) | func (i *Iterator) Value() (core.Event, error) {
FILE: eventstore/kurrent/iterator.go
type Iterator (line 10) | type Iterator struct
method Close (line 16) | func (i *Iterator) Close() {
method Next (line 21) | func (i *Iterator) Next() bool {
method Value (line 34) | func (i *Iterator) Value() (core.Event, error) {
FILE: eventstore/kurrent/kurrent.go
constant streamSeparator (line 10) | streamSeparator = "-"
type Kurrent (line 13) | type Kurrent struct
method Save (line 32) | func (es *Kurrent) Save(events []core.Event) error {
method Get (line 80) | func (es *Kurrent) Get(ctx context.Context, id string, aggregateType s...
function Open (line 19) | func Open(client *kurrentdb.Client, jsonSerializer bool) *Kurrent {
function stream (line 96) | func stream(aggregateType, aggregateID string) string {
FILE: eventstore/kurrent/kurrent_test.go
function TestSuite (line 16) | func TestSuite(t *testing.T) {
FILE: eventstore/memory/iterator.go
type iterator (line 5) | type iterator struct
method Next (line 11) | func (i *iterator) Next() bool {
method Value (line 20) | func (i *iterator) Value() (core.Event, error) {
method Close (line 24) | func (i *iterator) Close() {
FILE: eventstore/memory/memory.go
type Memory (line 11) | type Memory struct
method Save (line 26) | func (e *Memory) Save(events []core.Event) error {
method Get (line 69) | func (e *Memory) Get(ctx context.Context, id string, aggregateType str...
method Close (line 84) | func (e *Memory) Close() {}
method globalEvents (line 92) | func (e *Memory) globalEvents(start core.Version, count uint64) ([]cor...
method All (line 112) | func (m *Memory) All(start core.Version, count uint64) core.Fetcher {
function Create (line 18) | func Create() *Memory {
function aggregateKey (line 87) | func aggregateKey(aggregateType, aggregateID string) string {
FILE: eventstore/memory/memory_test.go
function TestEventStore (line 11) | func TestEventStore(t *testing.T) {
function TestFetcherAll (line 19) | func TestFetcherAll(t *testing.T) {
FILE: eventstore/sql/iterator.go
type Iterator (line 10) | type Iterator struct
method Next (line 16) | func (i *Iterator) Next() bool {
method Value (line 21) | func (i *Iterator) Value() (core.Event, error) {
method Close (line 51) | func (i *Iterator) Close() {
FILE: eventstore/sql/migrate.go
function migrate (line 8) | func migrate(db *sql.DB, stm []string) error {
FILE: eventstore/sql/postgres.go
type Postgres (line 29) | type Postgres struct
method Close (line 47) | func (s *Postgres) Close() {
method Save (line 52) | func (s *Postgres) Save(events []core.Event) error {
method Get (line 105) | func (s *Postgres) Get(ctx context.Context, id string, aggregateType s...
method All (line 115) | func (s *Postgres) All(start core.Version) core.Fetcher {
function NewPostgres (line 35) | func NewPostgres(db *sql.DB) (*Postgres, error) {
FILE: eventstore/sql/postgres_test.go
function TestSuitePostgres (line 19) | func TestSuitePostgres(t *testing.T) {
function TestFetcherAllPostgres (line 36) | func TestFetcherAllPostgres(t *testing.T) {
function postgresServer (line 50) | func postgresServer() (string, func(), error) {
function postgreConnect (line 83) | func postgreConnect(dsn string) (*sql.Postgres, error) {
FILE: eventstore/sql/sqlite.go
type SQLite (line 30) | type SQLite struct
method Close (line 66) | func (s *SQLite) Close() {
method Save (line 71) | func (s *SQLite) Save(events []core.Event) error {
method Get (line 128) | func (s *SQLite) Get(ctx context.Context, id string, aggregateType str...
method All (line 138) | func (s *SQLite) All(start core.Version) core.Fetcher {
function NewSQLite (line 36) | func NewSQLite(db *sql.DB) (*SQLite, error) {
function NewSQLiteSingelWriter (line 55) | func NewSQLiteSingelWriter(db *sql.DB) (*SQLite, error) {
FILE: eventstore/sql/sqlite_test.go
function TestSuiteSQLite (line 15) | func TestSuiteSQLite(t *testing.T) {
function TestSuiteSQLiteSingelWriter (line 22) | func TestSuiteSQLiteSingelWriter(t *testing.T) {
function TestFetchFuncAll (line 28) | func TestFetchFuncAll(t *testing.T) {
function eventstore (line 37) | func eventstore(singelWriter bool) (*sql.SQLite, func(), error) {
FILE: eventstore/sql/sqlserver.go
constant createTableSQLServer (line 14) | createTableSQLServer = `IF OBJECT_ID('[events]', 'U') IS NULL
constant indexSQLServer (line 29) | indexSQLServer = `IF NOT EXISTS (
type SQLServer (line 44) | type SQLServer struct
method Close (line 60) | func (s *SQLServer) Close() {
method Save (line 65) | func (s *SQLServer) Save(events []core.Event) error {
method Get (line 129) | func (s *SQLServer) Get(ctx context.Context, id string, aggregateType ...
method All (line 142) | func (s *SQLServer) All(start core.Version) core.Fetcher {
function NewSQLServer (line 50) | func NewSQLServer(db *sql.DB) (*SQLServer, error) {
FILE: eventstore/sql/sqlserver_test.go
function TestSuiteSQLServer (line 19) | func TestSuiteSQLServer(t *testing.T) {
function TestFetcherAllSQLServer (line 36) | func TestFetcherAllSQLServer(t *testing.T) {
function sqlServerConnect (line 51) | func sqlServerConnect(dsn string) (*sql.SQLServer, error) {
function sqlServer (line 69) | func sqlServer() (string, func(), error) {
FILE: example/order/cmd/main.go
type Order (line 14) | type Order struct
function main (line 19) | func main() {
FILE: example/order/order.go
type Status (line 10) | type Status
constant Pending (line 13) | Pending Status = "pending"
constant Complete (line 14) | Complete Status = "complete"
type Order (line 27) | type Order struct
method Transition (line 37) | func (o *Order) Transition(event eventsourcing.Event) {
method Register (line 62) | func (o *Order) Register(r aggregate.RegisterFunc) {
method AddDiscount (line 110) | func (o *Order) AddDiscount(percentage uint) error {
method RemoveDiscount (line 134) | func (o *Order) RemoveDiscount() {
method Pay (line 145) | func (o *Order) Pay(amount uint) error {
type Created (line 73) | type Created struct
type DiscountApplied (line 78) | type DiscountApplied struct
type DiscountRemoved (line 84) | type DiscountRemoved struct
type Paid (line 87) | type Paid struct
type Completed (line 92) | type Completed struct
function Create (line 99) | func Create(amount uint) (*Order, error) {
FILE: example/order/order_test.go
function TestCreateOrder (line 9) | func TestCreateOrder(t *testing.T) {
function TestDiscount (line 30) | func TestDiscount(t *testing.T) {
function TestPaid (line 63) | func TestPaid(t *testing.T) {
FILE: example/tictactoe/cmd/main/main.go
function main (line 12) | func main() {
function PlayGame (line 23) | func PlayGame() *tictactoe.Game {
FILE: example/tictactoe/tictactoe.go
type Game (line 10) | type Game struct
method Transition (line 21) | func (g *Game) Transition(event eventsourcing.Event) {
method Register (line 44) | func (g *Game) Register(f aggregate.RegisterFunc) {
method Turn (line 82) | func (g *Game) Turn() string {
method Done (line 86) | func (g *Game) Done() bool {
method Winner (line 90) | func (g *Game) Winner() string {
method Render (line 95) | func (g *Game) Render() {
method PlayMove (line 115) | func (g *Game) PlayMove(x, y int) error {
type Started (line 51) | type Started struct
type XMoved (line 54) | type XMoved struct
type OMoved (line 60) | type OMoved struct
type XWon (line 66) | type XWon struct
type OWon (line 69) | type OWon struct
type Draw (line 72) | type Draw struct
function NewGame (line 75) | func NewGame() *Game {
function checkWinner (line 149) | func checkWinner(board [3][3]string) string {
function isDraw (line 172) | func isDraw(board [3][3]string) bool {
FILE: example/tictactoe/tictactoe_test.go
function TestValidMove (line 9) | func TestValidMove(t *testing.T) {
function TestInvalidMoveAlreadyTaken (line 34) | func TestInvalidMoveAlreadyTaken(t *testing.T) {
function TestTurnSwitching (line 43) | func TestTurnSwitching(t *testing.T) {
function TestWinDetection (line 51) | func TestWinDetection(t *testing.T) {
function TestDrawDetection (line 71) | func TestDrawDetection(t *testing.T) {
FILE: internal/encoderjson.go
type EncoderJSON (line 5) | type EncoderJSON struct
method Serialize (line 7) | func (e EncoderJSON) Serialize(v interface{}) ([]byte, error) {
method Deserialize (line 11) | func (e EncoderJSON) Deserialize(data []byte, v interface{}) error {
type encoder (line 15) | type encoder interface
FILE: internal/register.go
type register (line 11) | type register struct
method AggregateRegistered (line 37) | func (r *register) AggregateRegistered(a aggregate) bool {
method EventRegistered (line 45) | func (r *register) EventRegistered(event core.Event) (registerFunc, bo...
method Register (line 51) | func (r *register) Register(a aggregate) {
method RegisterAggregate (line 57) | func (r *register) RegisterAggregate(aggregateType string) func(events...
type aggregate (line 17) | type aggregate interface
function ResetRegister (line 25) | func ResetRegister() {
function newRegister (line 29) | func newRegister() *register {
function eventToFunc (line 79) | func eventToFunc(event interface{}) registerFunc {
function aggregateType (line 86) | func aggregateType(a aggregate) string {
FILE: iterator.go
type Iterator (line 10) | type Iterator struct
method Close (line 15) | func (i *Iterator) Close() {
method Next (line 19) | func (i *Iterator) Next() bool {
method Value (line 23) | func (i *Iterator) Value() (Event, error) {
FILE: projections.go
type callbackFunc (line 13) | type callbackFunc
type Projection (line 18) | type Projection struct
method TriggerAsync (line 57) | func (p *Projection) TriggerAsync() {
method TriggerSync (line 69) | func (p *Projection) TriggerSync() {
method Run (line 84) | func (p *Projection) Run(ctx context.Context, pace time.Duration) error {
method RunToEnd (line 118) | func (p *Projection) RunToEnd(ctx context.Context) ProjectionResult {
method RunOnce (line 145) | func (p *Projection) RunOnce() (bool, ProjectionResult) {
type ProjectionGroup (line 28) | type ProjectionGroup struct
method Start (line 193) | func (g *ProjectionGroup) Start() {
method TriggerAsync (line 211) | func (g *ProjectionGroup) TriggerAsync() {
method TriggerSync (line 218) | func (g *ProjectionGroup) TriggerSync() {
method Stop (line 231) | func (g *ProjectionGroup) Stop() {
type ProjectionResult (line 37) | type ProjectionResult struct
function NewProjection (line 44) | func NewProjection(fetchF core.Fetcher, callbackF callbackFunc) *Project...
function NewProjectionGroup (line 183) | func NewProjectionGroup(projections ...*Projection) *ProjectionGroup {
function ProjectionsRace (line 248) | func ProjectionsRace(cancelOnError bool, projections ...*Projection) ([]...
FILE: projections_test.go
type Person (line 19) | type Person struct
method Transition (line 46) | func (person *Person) Transition(event eventsourcing.Event) {
method Register (line 57) | func (person *Person) Register(f aggregate.RegisterFunc) {
method GrowOlder (line 62) | func (person *Person) GrowOlder() {
type Born (line 27) | type Born struct
type AgedOneYear (line 32) | type AgedOneYear struct
function CreatePerson (line 36) | func CreatePerson(name string) (*Person, error) {
function createPersonEvent (line 68) | func createPersonEvent(es *memory.Memory, name string, age int) error {
function TestRunOnce (line 98) | func TestRunOnce(t *testing.T) {
function TestRun (line 151) | func TestRun(t *testing.T) {
function TestRunSameProjectionConcurrently (line 186) | func TestRunSameProjectionConcurrently(t *testing.T) {
function TestTriggerSync (line 223) | func TestTriggerSync(t *testing.T) {
function TestTriggerAsync (line 267) | func TestTriggerAsync(t *testing.T) {
function TestCloseEmptyGroup (line 320) | func TestCloseEmptyGroup(t *testing.T) {
function TestStartMultipleProjections (line 328) | func TestStartMultipleProjections(t *testing.T) {
function TestErrorFromCallback (line 347) | func TestErrorFromCallback(t *testing.T) {
function TestStrict (line 386) | func TestStrict(t *testing.T) {
function TestRace (line 407) | func TestRace(t *testing.T) {
function TestKeepStartPosition (line 457) | func TestKeepStartPosition(t *testing.T) {
FILE: snapshotstore/memory/memory.go
type Memory (line 9) | type Memory struct
method Close (line 20) | func (m *Memory) Close() {
method Get (line 24) | func (m *Memory) Get(ctx context.Context, aggregateID, aggregateType s...
method Save (line 32) | func (m *Memory) Save(snapshot core.Snapshot) error {
function Create (line 14) | func Create() *Memory {
FILE: snapshotstore/memory/memory_test.go
function TestSuite (line 11) | func TestSuite(t *testing.T) {
FILE: snapshotstore/sql/migrate.go
function migrate (line 8) | func migrate(db *sql.DB, stm []string) error {
FILE: snapshotstore/sql/postgres.go
constant createTablePostgres (line 12) | createTablePostgres = `CREATE TABLE IF NOT EXISTS snapshots (
constant createIndexPostgres (line 20) | createIndexPostgres = `CREATE UNIQUE INDEX IF NOT EXISTS id_type ON snap...
type Postgres (line 22) | type Postgres struct
method Close (line 40) | func (s *Postgres) Close() {
method Save (line 45) | func (s *Postgres) Save(snapshot core.Snapshot) error {
method Get (line 77) | func (s *Postgres) Get(ctx context.Context, aggregateID, aggregateType...
function NewPostgres (line 27) | func NewPostgres(db *sql.DB) (*Postgres, error) {
FILE: snapshotstore/sql/postgres_test.go
function TestSuitePostgres (line 19) | func TestSuitePostgres(t *testing.T) {
FILE: snapshotstore/sql/sqlite.go
constant createTableSQLite (line 12) | createTableSQLite = `
constant createIndexSQLite (line 21) | createIndexSQLite = `CREATE UNIQUE INDEX IF NOT EXISTS id_type ON snapsh...
type SQLite (line 23) | type SQLite struct
method Close (line 41) | func (s *SQLite) Close() {
method Save (line 46) | func (s *SQLite) Save(snapshot core.Snapshot) error {
method Get (line 78) | func (s *SQLite) Get(ctx context.Context, aggregateID, aggregateType s...
function NewSQLite (line 28) | func NewSQLite(db *sql.DB) (*SQLite, error) {
FILE: snapshotstore/sql/sqlite_test.go
function TestSuite (line 13) | func TestSuite(t *testing.T) {
function snapshotstore (line 20) | func snapshotstore() (*sql.SQLite, func(), error) {
Condensed preview — 81 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (270K chars).
[
{
"path": ".github/dependabot.yml",
"chars": 927,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".github/workflows/go.yml",
"chars": 2440,
"preview": "# This workflow will build a golang project\n# For more information see: https://docs.github.com/en/actions/automating-bu"
},
{
"path": ".gitignore",
"chars": 49,
"preview": "example/example\n.idea\ncoverage.txt\n*.swp\ngo.work\n"
},
{
"path": "LICENSE",
"chars": 16761,
"preview": "__________________________________\n\nMozilla Public License Version 2.0\n==================================\n\n1. Definition"
},
{
"path": "Makefile",
"chars": 731,
"preview": "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 eventstor"
},
{
"path": "README.md",
"chars": 17151,
"preview": "# Overview\n\nThis set of modules is a post implementation of [@jen20's](https://github.com/jen20) way of implementing eve"
},
{
"path": "aggregate/aggregate.go",
"chars": 4144,
"preview": "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/ha"
},
{
"path": "aggregate/aggregate_test.go",
"chars": 2242,
"preview": "package aggregate_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing\"\n\t\"github.com/hallgren/event"
},
{
"path": "aggregate/idgenerator.go",
"chars": 873,
"preview": "package aggregate\n\nimport (\n\t\"crypto/rand\"\n)\n\n// idFunc is a global function that generates aggregate id's.\n// It could "
},
{
"path": "aggregate/root.go",
"chars": 3390,
"preview": "package aggregate\n\nimport (\n\t\"reflect\"\n\t\"time\"\n\n\t\"github.com/hallgren/eventsourcing\"\n\t\"github.com/hallgren/eventsourcing"
},
{
"path": "aggregate/root_test.go",
"chars": 5263,
"preview": "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/eventsourci"
},
{
"path": "aggregate/snapshot.go",
"chars": 2296,
"preview": "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/"
},
{
"path": "aggregate/snapshot_test.go",
"chars": 4409,
"preview": "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/hall"
},
{
"path": "core/event.go",
"chars": 539,
"preview": "package core\n\nimport (\n\t\"time\"\n)\n\n// Version is the event version used in event.Version and event.GlobalVersio\ntype Vers"
},
{
"path": "core/eventstore.go",
"chars": 561,
"preview": "package core\n\nimport (\n\t\"context\"\n\t\"errors\"\n)\n\n// ErrConcurrency when the currently saved version of the aggregate diffe"
},
{
"path": "core/fetcher.go",
"chars": 115,
"preview": "package core\n\n// Fetcher is the event fetch function concumed by projections\ntype Fetcher func() (Iterator, error)\n"
},
{
"path": "core/go.mod",
"chars": 55,
"preview": "module github.com/hallgren/eventsourcing/core\n\ngo 1.13\n"
},
{
"path": "core/snapshotstore.go",
"chars": 572,
"preview": "package core\n\nimport (\n\t\"context\"\n\t\"errors\"\n)\n\n// ErrSnapshotNotFound returned when no snapshot is found in the snapshot"
},
{
"path": "core/testsuite/eventstoresuite.go",
"chars": 9835,
"preview": "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\"gith"
},
{
"path": "core/testsuite/fetcher.go",
"chars": 2007,
"preview": "package testsuite\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\nfunc TestFetcher(t *testing."
},
{
"path": "core/testsuite/snapshotstoresuite.go",
"chars": 1833,
"preview": "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 sn"
},
{
"path": "event.go",
"chars": 1071,
"preview": "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"
},
{
"path": "eventsourcing.go",
"chars": 1746,
"preview": "package eventsourcing\n\nimport (\n\t\"errors\"\n\n\t\"github.com/hallgren/eventsourcing/internal\"\n)\n\nvar (\n\t// ErrAggregateNotFou"
},
{
"path": "eventsourcing_test.go",
"chars": 2419,
"preview": "package eventsourcing_test\n\n/*\nfunc TestGetWithContextCancel(t *testing.T) {\n\tes := memory.Create()\n\taggregate.Aggregate"
},
{
"path": "eventstore/bbolt/README.md",
"chars": 527,
"preview": "## BBolt Event Store\n\nThis event store supports the go.etcd.io/bbolt bolt driver.\n\n### Constructor\n\n```go\n// New opens t"
},
{
"path": "eventstore/bbolt/bbolt.go",
"chars": 6431,
"preview": "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/e"
},
{
"path": "eventstore/bbolt/bbolt_test.go",
"chars": 702,
"preview": "package bbolt_test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsour"
},
{
"path": "eventstore/bbolt/go.mod",
"chars": 283,
"preview": "module github.com/hallgren/eventsourcing/eventstore/bbolt\n\ngo 1.23\n\ntoolchain go1.23.6\n\nrequire (\n\tgithub.com/hallgren/e"
},
{
"path": "eventstore/bbolt/go.sum",
"chars": 1328,
"preview": "github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.m"
},
{
"path": "eventstore/bbolt/iterator.go",
"chars": 1349,
"preview": "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\""
},
{
"path": "eventstore/esdb/README.md",
"chars": 216,
"preview": "# esdb event store\n\nThe esdb event store is supporting the [EventStoreDB](https://www.eventstore.com) database.\n\nIt's ba"
},
{
"path": "eventstore/esdb/esdb.go",
"chars": 2745,
"preview": "package esdb\n\nimport (\n\t\"context\"\n\n\t\"github.com/EventStore/EventStore-Client-Go/v4/esdb\"\n\t\"github.com/hallgren/eventsour"
},
{
"path": "eventstore/esdb/esdb_test.go",
"chars": 1342,
"preview": "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/te"
},
{
"path": "eventstore/esdb/go.mod",
"chars": 3114,
"preview": "module github.com/hallgren/eventsourcing/eventstore/esdb\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/EventStore/EventStore-Client-"
},
{
"path": "eventstore/esdb/go.sum",
"chars": 14062,
"preview": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMI"
},
{
"path": "eventstore/esdb/iterator.go",
"chars": 1222,
"preview": "package esdb\n\nimport (\n\t\"strings\"\n\n\t\"github.com/EventStore/EventStore-Client-Go/v4/esdb\"\n\t\"github.com/hallgren/eventsour"
},
{
"path": "eventstore/kurrent/go.mod",
"chars": 3113,
"preview": "module github.com/hallgren/eventsourcing/eventstore/kurrent\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/hallgren/eventsourcing/cor"
},
{
"path": "eventstore/kurrent/go.sum",
"chars": 13813,
"preview": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMI"
},
{
"path": "eventstore/kurrent/iterator.go",
"chars": 1231,
"preview": "package kurrent\n\nimport (\n\t\"strings\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/kurrent-io/KurrentDB-Client"
},
{
"path": "eventstore/kurrent/kurrent.go",
"chars": 2849,
"preview": "package kurrent\n\nimport (\n\t\"context\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/kurrent-io/KurrentDB-Client"
},
{
"path": "eventstore/kurrent/kurrent_test.go",
"chars": 1368,
"preview": "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.co"
},
{
"path": "eventstore/memory/iterator.go",
"chars": 432,
"preview": "package memory\n\nimport \"github.com/hallgren/eventsourcing/core\"\n\ntype iterator struct {\n\tevents []core.Event\n\tposition"
},
{
"path": "eventstore/memory/memory.go",
"chars": 3497,
"preview": "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 eve"
},
{
"path": "eventstore/memory/memory_test.go",
"chars": 494,
"preview": "package memory_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/"
},
{
"path": "eventstore/sql/README.md",
"chars": 4600,
"preview": "# SQL Event Store\n\nThe sql eventstore is a module containing multiple sql based event stores that are all based on the\nd"
},
{
"path": "eventstore/sql/go.mod",
"chars": 3031,
"preview": "module github.com/hallgren/eventsourcing/eventstore/sql\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/denisenkom/go-mssqldb v0.12.3\n"
},
{
"path": "eventstore/sql/go.sum",
"chars": 16210,
"preview": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMI"
},
{
"path": "eventstore/sql/iterator.go",
"chars": 1087,
"preview": "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"
},
{
"path": "eventstore/sql/migrate.go",
"chars": 313,
"preview": "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.BeginT"
},
{
"path": "eventstore/sql/postgres.go",
"chars": 3640,
"preview": "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/c"
},
{
"path": "eventstore/sql/postgres_test.go",
"chars": 2270,
"preview": "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.c"
},
{
"path": "eventstore/sql/sqlite.go",
"chars": 4493,
"preview": "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/c"
},
{
"path": "eventstore/sql/sqlite_test.go",
"chars": 1460,
"preview": "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/co"
},
{
"path": "eventstore/sql/sqlserver.go",
"chars": 4390,
"preview": "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/c"
},
{
"path": "eventstore/sql/sqlserver_test.go",
"chars": 2277,
"preview": "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-mssq"
},
{
"path": "example/README.md",
"chars": 399,
"preview": "## Collection of reference projects\n\n* [Ardan labs](https://github.com/hallgren/ardanlabs) - Example when presenting eve"
},
{
"path": "example/go.mod",
"chars": 229,
"preview": "module github.com/hallgren/eventsourcing/example\n\ngo 1.22\n\nrequire github.com/hallgren/eventsourcing v0.8.1\n\nrequire git"
},
{
"path": "example/go.sum",
"chars": 565,
"preview": "github.com/hallgren/eventsourcing v0.8.0 h1:lFMWRdc59+tRwzCzmby8I3qr2WbD1GWqB/ypJrBMbVc=\ngithub.com/hallgren/eventsourci"
},
{
"path": "example/order/cmd/main.go",
"chars": 2401,
"preview": "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/eventsourci"
},
{
"path": "example/order/order.go",
"chars": 3624,
"preview": "package order\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/hallgren/eventsourcing\"\n\t\"github.com/hallgren/eventsourcing/aggregate\"\n)\n\nt"
},
{
"path": "example/order/order_test.go",
"chars": 2044,
"preview": "package order_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/example/order\"\n)\n\nfunc TestCreateOrder(t *t"
},
{
"path": "example/tictactoe/cmd/main/main.go",
"chars": 569,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\n\t\"github.com/hallgren/eventsourcing/aggregate\"\n\t\"github.com/hallgren/eventso"
},
{
"path": "example/tictactoe/tictactoe.go",
"chars": 3695,
"preview": "package tictactoe\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/hallgren/eventsourcing\"\n\t\"github.com/hallgren/eventsourcing/aggregate\"\n"
},
{
"path": "example/tictactoe/tictactoe_test.go",
"chars": 2395,
"preview": "package tictactoe_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/example/tictactoe\"\n)\n\nfunc TestValidMov"
},
{
"path": "go.mod",
"chars": 166,
"preview": "module github.com/hallgren/eventsourcing\n\ngo 1.19\n\nrequire github.com/hallgren/eventsourcing/core v0.5.2\n\n// replace git"
},
{
"path": "go.sum",
"chars": 195,
"preview": "github.com/hallgren/eventsourcing/core v0.5.2 h1:knvM1jP0zziiybce+Au7ysYvZQnDwxkV+/RFZWNDMiw=\ngithub.com/hallgren/events"
},
{
"path": "internal/encoderjson.go",
"chars": 538,
"preview": "package internal\n\nimport \"encoding/json\"\n\ntype EncoderJSON struct{}\n\nfunc (e EncoderJSON) Serialize(v interface{}) ([]by"
},
{
"path": "internal/register.go",
"chars": 2251,
"preview": "package internal\n\nimport (\n\t\"reflect\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\ntype registerFunc = func() interface"
},
{
"path": "iterator.go",
"chars": 1208,
"preview": "package eventsourcing\n\nimport (\n\t\"fmt\"\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/int"
},
{
"path": "projections.go",
"chars": 7373,
"preview": "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"
},
{
"path": "projections_test.go",
"chars": 11440,
"preview": "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/hall"
},
{
"path": "snapshotstore/memory/memory.go",
"chars": 674,
"preview": "package memory\n\nimport (\n\t\"context\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n)\n\ntype Memory struct {\n\tsnapshots map[st"
},
{
"path": "snapshotstore/memory/memory_test.go",
"chars": 383,
"preview": "package memory_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/hallgren/eventsourcing/"
},
{
"path": "snapshotstore/sql/README.md",
"chars": 1823,
"preview": "# SQL Snapshot Store\n\nThe sql is a module containing multiple sql based snapshot stores that are all based on the\ndataba"
},
{
"path": "snapshotstore/sql/go.mod",
"chars": 2867,
"preview": "module github.com/hallgren/eventsourcing/snapshotstore/sql\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/hallgren/eventsourcing/core"
},
{
"path": "snapshotstore/sql/go.sum",
"chars": 12656,
"preview": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMI"
},
{
"path": "snapshotstore/sql/migrate.go",
"chars": 313,
"preview": "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.BeginT"
},
{
"path": "snapshotstore/sql/postgres.go",
"chars": 2670,
"preview": "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 cr"
},
{
"path": "snapshotstore/sql/postgres_test.go",
"chars": 1833,
"preview": "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.c"
},
{
"path": "snapshotstore/sql/sqlite.go",
"chars": 2672,
"preview": "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 cr"
},
{
"path": "snapshotstore/sql/sqlite_test.go",
"chars": 787,
"preview": "package sql_test\n\nimport (\n\tsqldriver \"database/sql\"\n\t\"testing\"\n\n\t\"github.com/hallgren/eventsourcing/core\"\n\t\"github.com/"
}
]
About this extraction
This page contains the full source code of the hallgren/eventsourcing GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 81 files (245.2 KB), approximately 84.0k tokens, and a symbol index with 328 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.