Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Working conventions

These instructions apply to every contributor and coding harness.

## Releases

- Use one shared version for all four public modules: `github.com/jilio/ebu`,
`github.com/jilio/ebu/stores/sqlite`, `github.com/jilio/ebu/stores/durablestream`,
and `github.com/jilio/ebu/otel`. Never release a module independently.
- Publish the root tag `vX.Y.Z` and the corresponding `stores/sqlite/vX.Y.Z`,
`stores/durablestream/vX.Y.Z`, and `otel/vX.Y.Z` tags as one project-wide
release. Publish the root dependency before validating and publishing modules
that depend on its new version.
- Keep direct dependencies between these modules and the example modules'
dependency pins aligned with the shared release version.
- Compatible changes during `0.x` use a patch release. A minor release requires
a breaking change or an explicit owner decision; an additive API alone does
not justify a minor bump.
- Never rewrite or remove a published tag.
- Record changes in `CHANGELOG.md` and complete the formatting, tests, and
coverage checks required by [CLAUDE.md](CLAUDE.md) before opening a pull
request. Test each affected module; a root `go test ./...` does not cross
module boundaries.
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.20.1] - Unreleased

### Fixed

- Apply the same SQLite read-side page-cache, temporary-storage, memory-mapping,
and busy-timeout settings to every connection opened by `New` and
`OpenReadOnly`. Read-only connections retain their write protection and do
not request writer transaction locks or durability settings.
- Treat SQLite filenames containing `%`, `?`, or `#` literally in `New`, using
the same URI escaping as `OpenReadOnly`. The special `:memory:` behavior is
unchanged. Both constructors reject NUL bytes instead of opening a truncated
filename.

### Changed

- Document the shared release version for all four public modules and use
patch releases for compatible changes during `0.x`.
- Align nested-module and example dependency pins with the shared project
release.

## [0.20.0] - 2026-09-12

### Added

- SQLite `OpenReadOnly` opens existing stores without creating or migrating a
database or changing its journal mode. Unsupported schemas fail without
repair; mutations return `ErrReadOnly`. SQLite enforces read-only access on
every pooled connection while allowing later WAL commits to remain visible.
WAL shared-memory sidecars may still be created or updated.

### Changed

- Publish `0.20.0` as one shared release across the core, SQLite, Durable
Streams, and OpenTelemetry modules.

## [0.19.0] - 2026-09-05

### Added
Expand Down Expand Up @@ -920,6 +955,8 @@ Initial release of ebu (Event BUs) - a lightweight, type-safe event bus for Go.
- `ClearAll`: Remove all handlers
- `WaitAsync`: Wait for async handlers to complete

[0.20.1]: https://github.com/jilio/ebu/compare/v0.20.0...HEAD
[0.20.0]: https://github.com/jilio/ebu/compare/v0.19.0...v0.20.0
[0.19.0]: https://github.com/jilio/ebu/compare/v0.18.1...v0.19.0
[0.18.1]: https://github.com/jilio/ebu/compare/v0.18.0...v0.18.1
[0.18.0]: https://github.com/jilio/ebu/compare/v0.17.0...v0.18.0
Expand Down
15 changes: 14 additions & 1 deletion docs/PERSISTENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,20 @@ creating a database, migrating its schema, or changing its journal mode. The
stored schema version must be supported; an uninitialized or incompatible
database is rejected. `WithAutoMigrate(true)` is an error with this constructor.
`Append`, `SaveOffset`, `SaveSnapshot`, and `TruncateBefore` return
`sqlite.ErrReadOnly`. Ordinary `New` behavior is unchanged.
`sqlite.ErrReadOnly`. `New` continues to enable WAL and automatic migrations
by default.

Both constructors accept literal filenames, not SQLite URIs: characters such
as `%`, `?`, and `#` are escaped rather than interpreted as options. NUL bytes
are rejected. `New` also accepts the special path `:memory:` for an isolated,
pooled in-memory store; `OpenReadOnly` requires an existing file.

For file-backed stores, every pooled connection uses the same read-side
settings: a 64 MB page cache, memory-backed temporary storage, and up to
256 MB of memory-mapped reads. Both constructors honor `WithBusyTimeout`
(five seconds by default). Read-only connections additionally enforce
`query_only` and do not use the writer's synchronous or immediate-transaction
settings.

Read-only stores use SQLite `mode=ro`, so later commits from a concurrent WAL
writer remain visible. They do not use `immutable=1` and do not provide a fixed
Expand Down
4 changes: 2 additions & 2 deletions examples/observability/generator/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ module github.com/jilio/ebu/examples/observability/generator
go 1.25.12

require (
github.com/jilio/ebu v0.19.0
github.com/jilio/ebu/otel v0.19.0
github.com/jilio/ebu v0.20.0
github.com/jilio/ebu/otel v0.20.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0
Expand Down
8 changes: 4 additions & 4 deletions examples/observability/generator/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ 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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/jilio/ebu v0.19.0 h1:W2Bf5OyCsXH+a9qoWndN9OCGbgBJkunWj8TjKGkly78=
github.com/jilio/ebu v0.19.0/go.mod h1:HudFk9G56WhAmSpucnJFC7nf6/uSpCcEZYS2sItng74=
github.com/jilio/ebu/otel v0.19.0 h1:vh09+rlW8tkKgEpBPvIl/v+e4lxzfCy0pRALbK+8rPs=
github.com/jilio/ebu/otel v0.19.0/go.mod h1:1jHZ12PzAyACNljhX5tEYma1S2P70LVAGqGKenubgvI=
github.com/jilio/ebu v0.20.0 h1:zqhm01cWZQVsV+pVcUVO+xZBdf2OZ1rBxy3UvoSTnlE=
github.com/jilio/ebu v0.20.0/go.mod h1:HudFk9G56WhAmSpucnJFC7nf6/uSpCcEZYS2sItng74=
github.com/jilio/ebu/otel v0.20.0 h1:4WLujzd5yqJV1xinZxheQFxcnDm55PBqoFa5rPa0b7k=
github.com/jilio/ebu/otel v0.20.0/go.mod h1:1jHZ12PzAyACNljhX5tEYma1S2P70LVAGqGKenubgvI=
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
Expand Down
4 changes: 2 additions & 2 deletions examples/observability/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ module github.com/jilio/ebu/examples/observability
go 1.25.12

require (
github.com/jilio/ebu v0.19.0
github.com/jilio/ebu/otel v0.19.0
github.com/jilio/ebu v0.20.0
github.com/jilio/ebu/otel v0.20.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0
Expand Down
8 changes: 4 additions & 4 deletions examples/observability/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ 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/jilio/ebu v0.19.0 h1:W2Bf5OyCsXH+a9qoWndN9OCGbgBJkunWj8TjKGkly78=
github.com/jilio/ebu v0.19.0/go.mod h1:HudFk9G56WhAmSpucnJFC7nf6/uSpCcEZYS2sItng74=
github.com/jilio/ebu/otel v0.19.0 h1:vh09+rlW8tkKgEpBPvIl/v+e4lxzfCy0pRALbK+8rPs=
github.com/jilio/ebu/otel v0.19.0/go.mod h1:1jHZ12PzAyACNljhX5tEYma1S2P70LVAGqGKenubgvI=
github.com/jilio/ebu v0.20.0 h1:zqhm01cWZQVsV+pVcUVO+xZBdf2OZ1rBxy3UvoSTnlE=
github.com/jilio/ebu v0.20.0/go.mod h1:HudFk9G56WhAmSpucnJFC7nf6/uSpCcEZYS2sItng74=
github.com/jilio/ebu/otel v0.20.0 h1:4WLujzd5yqJV1xinZxheQFxcnDm55PBqoFa5rPa0b7k=
github.com/jilio/ebu/otel v0.20.0/go.mod h1:1jHZ12PzAyACNljhX5tEYma1S2P70LVAGqGKenubgvI=
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
Expand Down
2 changes: 1 addition & 1 deletion otel/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/jilio/ebu/otel
go 1.25.12

require (
github.com/jilio/ebu v0.19.0
github.com/jilio/ebu v0.20.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/metric v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
Expand Down
4 changes: 2 additions & 2 deletions otel/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ 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/jilio/ebu v0.19.0 h1:W2Bf5OyCsXH+a9qoWndN9OCGbgBJkunWj8TjKGkly78=
github.com/jilio/ebu v0.19.0/go.mod h1:HudFk9G56WhAmSpucnJFC7nf6/uSpCcEZYS2sItng74=
github.com/jilio/ebu v0.20.0 h1:zqhm01cWZQVsV+pVcUVO+xZBdf2OZ1rBxy3UvoSTnlE=
github.com/jilio/ebu v0.20.0/go.mod h1:HudFk9G56WhAmSpucnJFC7nf6/uSpCcEZYS2sItng74=
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
Expand Down
2 changes: 1 addition & 1 deletion stores/durablestream/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ go 1.25.1

require (
github.com/durable-streams/durable-streams/packages/client-go v0.2.0
github.com/jilio/ebu v0.19.0
github.com/jilio/ebu v0.20.0
)
4 changes: 2 additions & 2 deletions stores/durablestream/go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
github.com/durable-streams/durable-streams/packages/client-go v0.2.0 h1:s5QL0QEu06m954GVbV76Hl21sXCq2M9883PaoMI0oh4=
github.com/durable-streams/durable-streams/packages/client-go v0.2.0/go.mod h1:G9l/FuZz1LWA1IhuCg6epPrMKU7Yd9805L4yC+0iuHc=
github.com/jilio/ebu v0.19.0 h1:W2Bf5OyCsXH+a9qoWndN9OCGbgBJkunWj8TjKGkly78=
github.com/jilio/ebu v0.19.0/go.mod h1:HudFk9G56WhAmSpucnJFC7nf6/uSpCcEZYS2sItng74=
github.com/jilio/ebu v0.20.0 h1:zqhm01cWZQVsV+pVcUVO+xZBdf2OZ1rBxy3UvoSTnlE=
github.com/jilio/ebu v0.20.0/go.mod h1:HudFk9G56WhAmSpucnJFC7nf6/uSpCcEZYS2sItng74=
52 changes: 52 additions & 0 deletions stores/sqlite/dsn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package sqlite

import (
"fmt"
"net/url"
"path/filepath"
"strings"
)

// buildDSN configures each pooled connection, including connections opened
// after construction. Running connection-scoped pragmas through db.Exec would
// configure only the connection handed out by the pool at that moment.
func buildDSN(cfg *config) (string, error) {
if cfg.path == ":memory:" {
// Shared cache mode lets pooled connections see the same database.
// Each store gets a unique name so independent stores stay isolated.
return fmt.Sprintf("file:ebu_memdb_%d?mode=memory&cache=shared", memDBCounter.Add(1)), nil
}
// SQLite truncates decoded URI paths at NUL. Reject it before encoding so
// an invalid filename cannot open or create a different, shorter path.
if strings.ContainsRune(cfg.path, '\x00') {
return "", fmt.Errorf("sqlite: path cannot contain NUL")
}
abs, err := filepath.Abs(cfg.path)
if err != nil {
return "", fmt.Errorf("sqlite: resolve database path: %w", err)
}
// Both constructors share read-side settings. Writer durability and
// transaction locking belong only to New; OpenReadOnly never requests a
// journal-mode change or an immediate (write-locking) transaction.
query := url.Values{"_pragma": {
fmt.Sprintf("busy_timeout(%d)", cfg.busyTimeout.Milliseconds()),
"cache_size(-64000)", // 64MB cache
"temp_store(MEMORY)",
"mmap_size(268435456)", // 256MB mmap
}}
if cfg.readOnly {
query.Set("mode", "ro")
query.Add("_pragma", "query_only(1)")
} else {
query.Add("_pragma", "synchronous(NORMAL)")
// Immediate transactions take the write lock at BEGIN (honoring
// busy_timeout) instead of upgrading mid-transaction, which can
// return SQLITE_BUSY immediately. Only migrations use them here.
query.Set("_txlock", "immediate")
}
// Escape the filename and parameters separately. Literal %, ? and # must
// never redirect the file or inject SQLite options into either constructor.
// OmitHost also keeps Windows drive letters in the path, not an authority.
uri := url.URL{Scheme: "file", OmitHost: true, Path: filepath.ToSlash(abs), RawQuery: query.Encode()}
return uri.String(), nil
}
Loading
Loading