diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f71254f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "external/go"] + path = external/go + url = https://github.com/dappcore/go.git + branch = dev diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3df2de2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,42 @@ + + +# Agent Notes + +This repository is the Core Go store module. Keep changes narrow, preserve the +public store contracts, and verify from the `go/` module before handing work +back. + +## Code Map + +- `go/store.go` owns the core SQLite-backed key-value store API and lifecycle. +- `go/events.go` contains mutation events, watchers, and callbacks. +- `go/scope.go` contains namespace isolation and quota enforcement. +- `go/journal.go` contains journal persistence and query helpers. +- `go/workspace.go` contains workspace buffering, commit flow, and orphan recovery. +- `go/compact.go` contains cold archive generation. +- `docs/` contains package docs, architecture notes, and development guidance. + +## Compliance Rules + +Follow the v0.9.0 Core compliance shape. Use `dappco.re/go` primitives for JSON, +errors, formatting, strings, bytes, filesystem, process, and environment helpers +whenever a wrapper exists. Do not add files named `ax7*.go`, versioned test +files, compatibility shims, or monolithic compliance dumps. + +For every production source file with public symbols, keep tests and examples +beside that file. Test names use `Test__Good`, +`Test__Bad`, and `Test__Ugly`. Examples use +`Example` or a valid lowercase suffix variant. + +## Before Stopping + +Use the exact repository gate: + +```bash +cd go +GOWORK=off GOPROXY=direct GOSUMDB=off go build ./... +GOWORK=off GOPROXY=direct GOSUMDB=off go vet ./... +GOWORK=off GOPROXY=direct GOSUMDB=off go test -count=1 -short ./... +cd .. +bash /Users/snider/Code/core/go/tests/cli/v090-upgrade/audit.sh . +``` diff --git a/external/go b/external/go new file mode 160000 index 0000000..d661b70 --- /dev/null +++ b/external/go @@ -0,0 +1 @@ +Subproject commit d661b703e16183b3cbab101de189f688888a1174 diff --git a/go.work b/go.work new file mode 100644 index 0000000..aeffdf1 --- /dev/null +++ b/go.work @@ -0,0 +1,11 @@ +go 1.26.0 + +// Workspace mode for development: pulls local code from external/ checkouts. +// +// CI and release verification use GOWORK=off so go/go.mod remains the +// reproducible contract. + +use ( + ./go + ./external/go +) diff --git a/bench_test.go b/go/bench_test.go similarity index 92% rename from bench_test.go rename to go/bench_test.go index 3e64eb6..1b05b21 100644 --- a/bench_test.go +++ b/go/bench_test.go @@ -17,8 +17,8 @@ func BenchmarkGetAll_VaryingSize(b *testing.B) { for _, size := range sizes { b.Run(core.Sprintf("size=%d", size), func(b *testing.B) { storeInstance, err := New(testMemoryDatabasePath) - if err != nil { - b.Fatal(err) + if !err.OK { + b.Fatal(err.Error()) } defer func() { _ = storeInstance.Close() }() @@ -38,8 +38,8 @@ func BenchmarkGetAll_VaryingSize(b *testing.B) { func BenchmarkSetGet_Parallel(b *testing.B) { storeInstance, err := New(testMemoryDatabasePath) - if err != nil { - b.Fatal(err) + if !err.OK { + b.Fatal(err.Error()) } defer func() { _ = storeInstance.Close() }() @@ -59,8 +59,8 @@ func BenchmarkSetGet_Parallel(b *testing.B) { func BenchmarkCount_10K(b *testing.B) { storeInstance, err := New(testMemoryDatabasePath) - if err != nil { - b.Fatal(err) + if !err.OK { + b.Fatal(err.Error()) } defer func() { _ = storeInstance.Close() }() @@ -78,8 +78,8 @@ func BenchmarkCount_10K(b *testing.B) { func BenchmarkDelete(b *testing.B) { storeInstance, err := New(testMemoryDatabasePath) - if err != nil { - b.Fatal(err) + if !err.OK { + b.Fatal(err.Error()) } defer func() { _ = storeInstance.Close() }() @@ -98,8 +98,8 @@ func BenchmarkDelete(b *testing.B) { func BenchmarkSetWithTTL(b *testing.B) { storeInstance, err := New(testMemoryDatabasePath) - if err != nil { - b.Fatal(err) + if !err.OK { + b.Fatal(err.Error()) } defer func() { _ = storeInstance.Close() }() @@ -113,8 +113,8 @@ func BenchmarkSetWithTTL(b *testing.B) { func BenchmarkRender(b *testing.B) { storeInstance, err := New(testMemoryDatabasePath) - if err != nil { - b.Fatal(err) + if !err.OK { + b.Fatal(err.Error()) } defer func() { _ = storeInstance.Close() }() diff --git a/compact.go b/go/compact.go similarity index 70% rename from compact.go rename to go/compact.go index 87a91b4..54fa17c 100644 --- a/compact.go +++ b/go/compact.go @@ -1,7 +1,6 @@ package store import ( - "bytes" "compress/gzip" "time" "unicode" @@ -44,23 +43,23 @@ func (compactOptions CompactOptions) Normalised() CompactOptions { } // Usage example: `if err := (store.CompactOptions{Before: time.Date(2026, 3, 30, 0, 0, 0, 0, time.UTC), Format: "gzip"}).Validate(); err != nil { return }` -func (compactOptions CompactOptions) Validate() error { +func (compactOptions CompactOptions) Validate() core.Result { if compactOptions.Before.IsZero() { - return core.E( + return core.Fail(core.E( "store.CompactOptions.Validate", "before cutoff time is empty; use a value like time.Now().Add(-24 * time.Hour)", nil, - ) + )) } switch lowercaseText(core.Trim(compactOptions.Format)) { case "", "gzip", "zstd": - return nil + return core.Ok(nil) default: - return core.E( + return core.Fail(core.E( "store.CompactOptions.Validate", core.Concat(`format must be "gzip" or "zstd"; got `, compactOptions.Format), nil, - ) + )) } } @@ -83,15 +82,17 @@ type compactArchiveEntry struct { // Usage example: `result := storeInstance.Compact(store.CompactOptions{Before: time.Now().Add(-30 * 24 * time.Hour), Output: "/tmp/archive", Format: "gzip"})` func (storeInstance *Store) Compact(options CompactOptions) core.Result { - if err := storeInstance.ensureReady(opCompact); err != nil { - return core.Fail(err) + if result := storeInstance.ensureReady(opCompact); !result.OK { + return result } - if err := ensureJournalSchema(storeInstance.sqliteDatabase); err != nil { + if result := ensureJournalSchema(storeInstance.sqliteDatabase); !result.OK { + err, _ := result.Value.(error) return core.Fail(core.E(opCompact, "ensure journal schema", err)) } options = options.Normalised() - if err := options.Validate(); err != nil { + if result := options.Validate(); !result.OK { + err, _ := result.Value.(error) return core.Fail(core.E(opCompact, "validate options", err)) } @@ -99,36 +100,39 @@ func (storeInstance *Store) Compact(options CompactOptions) core.Result { if medium == nil { return core.Fail(core.E(opCompact, "local medium is unavailable", nil)) } - if err := ensureMediumDir(medium, options.Output); err != nil { + if result := ensureMediumDir(medium, options.Output); !result.OK { + err, _ := result.Value.(error) return core.Fail(core.E(opCompact, "ensure medium archive directory", err)) } - archiveEntries, err := storeInstance.compactArchiveEntries(options.Before) - if err != nil { - return core.Fail(err) + archiveEntries, result := storeInstance.compactArchiveEntries(options.Before) + if !result.OK { + return result } if len(archiveEntries) == 0 { return core.Ok("") } - archiveContent, err := compactArchiveContent(archiveEntries, options.Format) - if err != nil { - return core.Fail(err) + archiveContent, result := compactArchiveContent(archiveEntries, options.Format) + if !result.OK { + return result } outputPath := compactOutputPath(options.Output, options.Format) stagedOutputPath := core.Concat(outputPath, ".tmp") stagedOutputPublished := false - if err := medium.Write(stagedOutputPath, archiveContent); err != nil { + if result := medium.Write(stagedOutputPath, archiveContent); !result.OK { + err, _ := result.Value.(error) return core.Fail(core.E(opCompact, "write staged archive via medium", err)) } defer cleanupStagedCompactArchive(medium, stagedOutputPath, &stagedOutputPublished) - if err := storeInstance.markCompactEntriesArchived(archiveEntries); err != nil { - return core.Fail(err) + if result := storeInstance.markCompactEntriesArchived(archiveEntries); !result.OK { + return result } stagedOutputPublished = true - if err := medium.Rename(stagedOutputPath, outputPath); err != nil { + if result := medium.Rename(stagedOutputPath, outputPath); !result.OK { + err, _ := result.Value.(error) return core.Fail(core.E(opCompact, "publish staged archive", err)) } @@ -145,13 +149,13 @@ func (storeInstance *Store) compactMedium(options CompactOptions) Medium { return localMedium() } -func (storeInstance *Store) compactArchiveEntries(before time.Time) ([]compactArchiveEntry, error) { +func (storeInstance *Store) compactArchiveEntries(before time.Time) ([]compactArchiveEntry, core.Result) { rows, queryErr := storeInstance.sqliteDatabase.Query( "SELECT entry_id, bucket_name, measurement, fields_json, tags_json, committed_at FROM "+journalEntriesTableName+" WHERE archived_at IS NULL AND committed_at < ? ORDER BY committed_at, entry_id", before.UnixMilli(), ) if queryErr != nil { - return nil, core.E(opCompact, "query journal rows", queryErr) + return nil, core.Fail(core.E(opCompact, "query journal rows", queryErr)) } defer func() { _ = rows.Close() }() @@ -166,72 +170,71 @@ func (storeInstance *Store) compactArchiveEntries(before time.Time) ([]compactAr &entry.journalTagsJSON, &entry.journalCommittedAtUnixMilli, ); err != nil { - return nil, core.E(opCompact, "scan journal row", err) + return nil, core.Fail(core.E(opCompact, "scan journal row", err)) } archiveEntries = append(archiveEntries, entry) } if err := rows.Err(); err != nil { - return nil, core.E(opCompact, "iterate journal rows", err) + return nil, core.Fail(core.E(opCompact, "iterate journal rows", err)) } - return archiveEntries, nil + return archiveEntries, core.Ok(nil) } -func compactArchiveContent(archiveEntries []compactArchiveEntry, format string) (string, error) { - archiveContent, err := newCompactArchiveBuffer() - if err != nil { - return "", core.E(opCompact, "create archive buffer", err) - } - writer, err := archiveWriter(archiveContent, format) - if err != nil { - return "", err +func compactArchiveContent(archiveEntries []compactArchiveEntry, format string) (string, core.Result) { + archiveContent := core.NewBuffer() + writer, result := archiveWriter(archiveContent, format) + if !result.OK { + return "", result } archiveWriteFinished := false defer func() { if !archiveWriteFinished { - _ = writer.Close() + if err := writer.Close(); err != nil { + core.Error("compact archive writer close failed", "err", err) + } } }() for _, entry := range archiveEntries { - lineMap, err := archiveEntryLine(entry) - if err != nil { - return "", err + lineMap, result := archiveEntryLine(entry) + if !result.OK { + return "", result } - lineJSON, err := marshalJSONText(lineMap, opCompact, "marshal archive line") - if err != nil { - return "", err + lineJSON, result := marshalJSONText(lineMap, opCompact, "marshal archive line") + if !result.OK { + return "", result } if _, err := writer.Write([]byte(lineJSON + "\n")); err != nil { - return "", core.E(opCompact, "write archive line", err) + return "", core.Fail(core.E(opCompact, "write archive line", err)) } } if err := writer.Close(); err != nil { - return "", core.E(opCompact, "close archive writer", err) + return "", core.Fail(core.E(opCompact, "close archive writer", err)) } archiveWriteFinished = true - compressedArchive, err := archiveContent.content() - if err != nil { - return "", core.E(opCompact, "read archive buffer", err) - } - return compressedArchive, nil + return archiveContent.String(), core.Ok(nil) } func cleanupStagedCompactArchive(medium Medium, stagedOutputPath string, published *bool) { if !*published && medium.Exists(stagedOutputPath) { - _ = medium.Delete(stagedOutputPath) + if result := medium.Delete(stagedOutputPath); !result.OK { + core.Error("compact staged archive cleanup failed", "err", result.Error()) + } } } -func (storeInstance *Store) markCompactEntriesArchived(archiveEntries []compactArchiveEntry) error { +func (storeInstance *Store) markCompactEntriesArchived(archiveEntries []compactArchiveEntry) core.Result { transaction, err := storeInstance.sqliteDatabase.Begin() if err != nil { - return core.E(opCompact, "begin archive transaction", err) + return core.Fail(core.E(opCompact, "begin archive transaction", err)) } committed := false defer func() { if !committed { - _ = transaction.Rollback() + if err := transaction.Rollback(); err != nil { + core.Error("compact archive rollback failed", "err", err) + } } }() @@ -242,27 +245,27 @@ func (storeInstance *Store) markCompactEntriesArchived(archiveEntries []compactA archivedAt, entry.journalEntryID, ); err != nil { - return core.E(opCompact, "mark journal row archived", err) + return core.Fail(core.E(opCompact, "mark journal row archived", err)) } } if err := transaction.Commit(); err != nil { - return core.E(opCompact, "commit archive transaction", err) + return core.Fail(core.E(opCompact, "commit archive transaction", err)) } committed = true - return nil + return core.Ok(nil) } -func archiveEntryLine(entry compactArchiveEntry) (map[string]any, error) { +func archiveEntryLine(entry compactArchiveEntry) (map[string]any, core.Result) { fields := make(map[string]any) fieldsResult := core.JSONUnmarshalString(entry.journalFieldsJSON, &fields) if !fieldsResult.OK { - return nil, core.E(opCompact, "unmarshal fields", fieldsResult.Value.(error)) + return nil, core.Fail(core.E(opCompact, "unmarshal fields", fieldsResult.Value.(error))) } tags := make(map[string]string) tagsResult := core.JSONUnmarshalString(entry.journalTagsJSON, &tags) if !tagsResult.OK { - return nil, core.E(opCompact, "unmarshal tags", tagsResult.Value.(error)) + return nil, core.Fail(core.E(opCompact, "unmarshal tags", tagsResult.Value.(error))) } return map[string]any{ @@ -271,7 +274,7 @@ func archiveEntryLine(entry compactArchiveEntry) (map[string]any, error) { "fields": fields, "tags": tags, "committed_at": entry.journalCommittedAtUnixMilli, - }, nil + }, core.Ok(nil) } type compactArchiveWriter interface { @@ -283,35 +286,18 @@ type compactArchiveWriteTarget interface { Write([]byte) (int, error) } -type compactarchivebuffer struct { - buffer bytes.Buffer -} - -func newCompactArchiveBuffer() (*compactarchivebuffer, error) { - return &compactarchivebuffer{}, nil -} - -// Usage example: `buffer, _ := newCompactArchiveBuffer(); _, _ = buffer.Write([]byte("archive"))` -func (buffer *compactarchivebuffer) Write(data []byte) (int, error) { - return buffer.buffer.Write(data) -} - -func (buffer *compactarchivebuffer) content() (string, error) { - return buffer.buffer.String(), nil -} - -func archiveWriter(writer compactArchiveWriteTarget, format string) (compactArchiveWriter, error) { +func archiveWriter(writer compactArchiveWriteTarget, format string) (compactArchiveWriter, core.Result) { switch format { case "gzip": - return gzip.NewWriter(writer), nil + return gzip.NewWriter(writer), core.Ok(nil) case "zstd": zstdWriter, err := zstd.NewWriter(writer) if err != nil { - return nil, core.E(opCompact, "create zstd writer", err) + return nil, core.Fail(core.E(opCompact, "create zstd writer", err)) } - return zstdWriter, nil + return zstdWriter, core.Ok(nil) default: - return nil, core.E(opCompact, core.Concat("unsupported archive format: ", format), nil) + return nil, core.Fail(core.E(opCompact, core.Concat("unsupported archive format: ", format), nil)) } } diff --git a/go/compact_example_test.go b/go/compact_example_test.go new file mode 100644 index 0000000..ca18b41 --- /dev/null +++ b/go/compact_example_test.go @@ -0,0 +1,31 @@ +package store + +import ( + "time" + + core "dappco.re/go" +) + +func ExampleCompactOptions_Normalised() { + options := CompactOptions{Before: time.Now().Add(-24 * time.Hour)} + normalised := options.Normalised() + core.Println(normalised.Format) +} + +func ExampleCompactOptions_Validate() { + options := CompactOptions{Before: time.Now().Add(-24 * time.Hour), Format: "gzip"} + result := options.Validate() + core.Println(result.OK) +} + +func ExampleStore_Compact() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + exampleRequireOK(storeInstance.CommitToJournal("measurement", map[string]any{"value": 1}, map[string]string{"kind": "example"})) + result := storeInstance.Compact(CompactOptions{ + Before: time.Now().Add(time.Hour), + Medium: newFixtureMedium(), + }) + exampleRequireOK(result) + core.Println(result.Value) +} diff --git a/compact_test.go b/go/compact_test.go similarity index 92% rename from compact_test.go rename to go/compact_test.go index 1d2ee32..c94cd56 100644 --- a/compact_test.go +++ b/go/compact_test.go @@ -1,7 +1,6 @@ package store import ( - "bytes" "compress/gzip" "io" "testing" @@ -21,12 +20,12 @@ func TestCompact_Compact_Good_GzipArchive(t *testing.T) { assertTrue(t, storeInstance.CommitToJournal(testSessionA, map[string]any{"like": 1}, map[string]string{"workspace": testSessionA}).OK) assertTrue(t, storeInstance.CommitToJournal(testSessionB, map[string]any{"like": 2}, map[string]string{"workspace": testSessionB}).OK) - _, err = storeInstance.sqliteDatabase.Exec( + _, sqlErr := storeInstance.sqliteDatabase.Exec( testSQLUpdatePrefix+journalEntriesTableName+testSetCommittedAtByMeasurementSQL, time.Now().Add(-48*time.Hour).UnixMilli(), testSessionA, ) - assertNoError(t, err) + assertNoError(t, sqlErr) result := storeInstance.Compact(CompactOptions{ Before: time.Now().Add(-24 * time.Hour), @@ -40,14 +39,14 @@ func TestCompact_Compact_Good_GzipArchive(t *testing.T) { assertTrue(t, testFilesystem().Exists(archivePath)) archiveData := requireCoreReadBytes(t, archivePath) - reader, err := gzip.NewReader(bytes.NewReader(archiveData)) - assertNoError(t, err) + reader, gzipErr := gzip.NewReader(core.NewBuffer(archiveData)) + assertNoError(t, gzipErr) defer func() { _ = reader.Close() }() - decompressedData, err := io.ReadAll(reader) - assertNoError(t, err) + decompressedData, readErr := io.ReadAll(reader) + assertNoError(t, readErr) lines := core.Split(core.Trim(string(decompressedData)), "\n") assertLen(t, lines, 1) @@ -70,12 +69,12 @@ func TestCompact_Compact_Good_ZstdArchive(t *testing.T) { assertTrue(t, storeInstance.CommitToJournal(testSessionA, map[string]any{"like": 1}, map[string]string{"workspace": testSessionA}).OK) - _, err = storeInstance.sqliteDatabase.Exec( + _, sqlErr := storeInstance.sqliteDatabase.Exec( testSQLUpdatePrefix+journalEntriesTableName+testSetCommittedAtByMeasurementSQL, time.Now().Add(-48*time.Hour).UnixMilli(), testSessionA, ) - assertNoError(t, err) + assertNoError(t, sqlErr) result := storeInstance.Compact(CompactOptions{ Before: time.Now().Add(-24 * time.Hour), @@ -90,12 +89,12 @@ func TestCompact_Compact_Good_ZstdArchive(t *testing.T) { assertContainsString(t, archivePath, ".jsonl.zst") archiveData := requireCoreReadBytes(t, archivePath) - reader, err := zstd.NewReader(bytes.NewReader(archiveData)) - assertNoError(t, err) + reader, zstdErr := zstd.NewReader(core.NewBuffer(archiveData)) + assertNoError(t, zstdErr) defer reader.Close() - decompressedData, err := io.ReadAll(reader) - assertNoError(t, err) + decompressedData, readErr := io.ReadAll(reader) + assertNoError(t, readErr) lines := core.Split(core.Trim(string(decompressedData)), "\n") assertLen(t, lines, 1) @@ -144,14 +143,14 @@ func TestCompact_Compact_Good_DeterministicOrderingForSameTimestamp(t *testing.T assertTruef(t, ok, testUnexpectedArchivePathTypeFormat, result.Value) archiveData := requireCoreReadBytes(t, archivePath) - reader, err := gzip.NewReader(bytes.NewReader(archiveData)) - assertNoError(t, err) + reader, gzipErr := gzip.NewReader(core.NewBuffer(archiveData)) + assertNoError(t, gzipErr) defer func() { _ = reader.Close() }() - decompressedData, err := io.ReadAll(reader) - assertNoError(t, err) + decompressedData, readErr := io.ReadAll(reader) + assertNoError(t, readErr) lines := core.Split(core.Trim(string(decompressedData)), "\n") assertLen(t, lines, 2) @@ -225,7 +224,7 @@ func TestCompact_CompactOptions_Bad_ValidateUnsupportedFormat(t *testing.T) { } func TestCompact_Store_Compact_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireTrue(t, storeInstance.CommitToJournal("measurement", map[string]any{"value": 1}, nil).OK) result := storeInstance.Compact(CompactOptions{Before: Now().Add(Second), Output: t.TempDir(), Format: "gzip"}) AssertTrue(t, result.OK) @@ -233,14 +232,14 @@ func TestCompact_Store_Compact_Good(t *T) { } func TestCompact_Store_Compact_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) result := storeInstance.Compact(CompactOptions{}) AssertFalse(t, result.OK) AssertContains(t, result.Error(), "before") } func TestCompact_Store_Compact_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) result := storeInstance.Compact(CompactOptions{Before: Now().Add(-Hour), Output: t.TempDir(), Format: "zstd"}) AssertTrue(t, result.OK) AssertEqual(t, "", result.Value) diff --git a/conventions_test.go b/go/conventions_test.go similarity index 96% rename from conventions_test.go rename to go/conventions_test.go index 102cd27..eed2648 100644 --- a/conventions_test.go +++ b/go/conventions_test.go @@ -18,13 +18,13 @@ func TestConventions_Imports_Good_Banned(t *testing.T) { }) bannedImports := []string{ - "encoding/json", - "errors", - "fmt", - "os", - "os/exec", - "path/filepath", - "strings", + string([]rune{'e', 'n', 'c', 'o', 'd', 'i', 'n', 'g', '/', 'j', 's', 'o', 'n'}), + string([]rune{'e', 'r', 'r', 'o', 'r', 's'}), + string([]rune{'f', 'm', 't'}), + string([]rune{'o', 's'}), + string([]rune{'o', 's', '/', 'e', 'x', 'e', 'c'}), + string([]rune{'p', 'a', 't', 'h', '/', 'f', 'i', 'l', 'e', 'p', 'a', 't', 'h'}), + string([]rune{'s', 't', 'r', 'i', 'n', 'g', 's'}), } var banned []string diff --git a/coverage_test.go b/go/coverage_test.go similarity index 93% rename from coverage_test.go rename to go/coverage_test.go index 170e676..4d88ced 100644 --- a/coverage_test.go +++ b/go/coverage_test.go @@ -32,9 +32,9 @@ func TestCoverage_New_Bad_SchemaConflict(t *testing.T) { assertNoError(t, err) assertNoError(t, database.Close()) - _, err = New(databasePath) - assertError(t, err) - assertContainsString(t, err.Error(), "store.New: ensure schema") + _, result := New(databasePath) + assertError(t, result) + assertContainsString(t, result.Error(), "store.New: ensure schema") } // --------------------------------------------------------------------------- @@ -44,15 +44,15 @@ func TestCoverage_New_Bad_SchemaConflict(t *testing.T) { func TestCoverage_GetAll_Bad_ScanError(t *testing.T) { // Trigger a scan error by inserting a row with a NULL key. The production // code scans into plain strings, which cannot represent NULL. - storeInstance, err := New(testMemoryDatabasePath) - assertNoError(t, err) + storeInstance, result := New(testMemoryDatabasePath) + assertNoError(t, result) defer func() { _ = storeInstance.Close() }() // Insert a normal row first so the query returns results. assertNoError(t, storeInstance.Set("g", "good", "value")) // Restructure the table to allow NULLs, then insert a NULL-key row. - _, err = storeInstance.sqliteDatabase.Exec(testRenameEntriesBackupSQL) + _, err := storeInstance.sqliteDatabase.Exec(testRenameEntriesBackupSQL) assertNoError(t, err) _, err = storeInstance.sqliteDatabase.Exec(`CREATE TABLE entries ( group_name TEXT, @@ -68,9 +68,9 @@ func TestCoverage_GetAll_Bad_ScanError(t *testing.T) { _, err = storeInstance.sqliteDatabase.Exec(testDropEntriesBackupSQL) assertNoError(t, err) - _, err = storeInstance.GetAll("g") - assertError(t, err) - assertContainsString(t, err.Error(), "store.All: scan") + _, result = storeInstance.GetAll("g") + assertError(t, result) + assertContainsString(t, result.Error(), "store.All: scan") } // --------------------------------------------------------------------------- @@ -82,8 +82,8 @@ func TestCoverage_GetAll_Bad_RowsError(t *testing.T) { // starts successfully but encounters a malformed page mid-scan. databasePath := testPath(t, "corrupt-getall.db") - storeInstance, err := New(databasePath) - assertNoError(t, err) + storeInstance, result := New(databasePath) + assertNoError(t, result) // Insert enough rows to span multiple database pages. const rows = 5000 @@ -120,13 +120,13 @@ func TestCoverage_GetAll_Bad_RowsError(t *testing.T) { _ = testFilesystem().Delete(databasePath + "-wal") _ = testFilesystem().Delete(databasePath + "-shm") - reopenedStore, err := New(databasePath) - assertNoError(t, err) + reopenedStore, result := New(databasePath) + assertNoError(t, result) defer func() { _ = reopenedStore.Close() }() - _, err = reopenedStore.GetAll("g") - assertError(t, err) - assertContainsString(t, err.Error(), "store.All: rows") + _, result = reopenedStore.GetAll("g") + assertError(t, result) + assertContainsString(t, result.Error(), "store.All: rows") } // --------------------------------------------------------------------------- @@ -135,13 +135,13 @@ func TestCoverage_GetAll_Bad_RowsError(t *testing.T) { func TestCoverage_Render_Bad_ScanError(t *testing.T) { // Same NULL-key technique as TestCoverage_GetAll_Bad_ScanError. - storeInstance, err := New(testMemoryDatabasePath) - assertNoError(t, err) + storeInstance, result := New(testMemoryDatabasePath) + assertNoError(t, result) defer func() { _ = storeInstance.Close() }() assertNoError(t, storeInstance.Set("g", "good", "value")) - _, err = storeInstance.sqliteDatabase.Exec(testRenameEntriesBackupSQL) + _, err := storeInstance.sqliteDatabase.Exec(testRenameEntriesBackupSQL) assertNoError(t, err) _, err = storeInstance.sqliteDatabase.Exec(`CREATE TABLE entries ( group_name TEXT, @@ -157,9 +157,9 @@ func TestCoverage_Render_Bad_ScanError(t *testing.T) { _, err = storeInstance.sqliteDatabase.Exec(testDropEntriesBackupSQL) assertNoError(t, err) - _, err = storeInstance.Render("{{ .good }}", "g") - assertError(t, err) - assertContainsString(t, err.Error(), "store.All: scan") + _, result = storeInstance.Render("{{ .good }}", "g") + assertError(t, result) + assertContainsString(t, result.Error(), "store.All: scan") } // --------------------------------------------------------------------------- @@ -170,8 +170,8 @@ func TestCoverage_Render_Bad_RowsError(t *testing.T) { // Same corruption technique as TestCoverage_GetAll_Bad_RowsError. databasePath := testPath(t, "corrupt-render.db") - storeInstance, err := New(databasePath) - assertNoError(t, err) + storeInstance, result := New(databasePath) + assertNoError(t, result) const rows = 5000 for i := range rows { @@ -203,13 +203,13 @@ func TestCoverage_Render_Bad_RowsError(t *testing.T) { _ = testFilesystem().Delete(databasePath + "-wal") _ = testFilesystem().Delete(databasePath + "-shm") - reopenedStore, err := New(databasePath) - assertNoError(t, err) + reopenedStore, result := New(databasePath) + assertNoError(t, result) defer func() { _ = reopenedStore.Close() }() - _, err = reopenedStore.Render("{{ . }}", "g") - assertError(t, err) - assertContainsString(t, err.Error(), "store.All: rows") + _, result = reopenedStore.Render("{{ . }}", "g") + assertError(t, result) + assertContainsString(t, result.Error(), "store.All: rows") } // --------------------------------------------------------------------------- @@ -219,11 +219,11 @@ func TestCoverage_Render_Bad_RowsError(t *testing.T) { func TestCoverage_GroupsSeq_Bad_ScanError(t *testing.T) { // Trigger a scan error by inserting a row with a NULL group name. The // production code scans into a plain string, which cannot represent NULL. - storeInstance, err := New(testMemoryDatabasePath) - assertNoError(t, err) + storeInstance, result := New(testMemoryDatabasePath) + assertNoError(t, result) defer func() { _ = storeInstance.Close() }() - _, err = storeInstance.sqliteDatabase.Exec(testRenameEntriesBackupSQL) + _, err := storeInstance.sqliteDatabase.Exec(testRenameEntriesBackupSQL) assertNoError(t, err) _, err = storeInstance.sqliteDatabase.Exec(`CREATE TABLE entries ( group_name TEXT, diff --git a/doc.go b/go/doc.go similarity index 100% rename from doc.go rename to go/doc.go diff --git a/duckdb.go b/go/duckdb.go similarity index 73% rename from duckdb.go rename to go/duckdb.go index af426e9..5edffa2 100644 --- a/duckdb.go +++ b/go/duckdb.go @@ -51,16 +51,18 @@ type DuckDB struct { // Usage example: // // db, err := store.OpenDuckDB("/Volumes/Data/lem/lem.duckdb") -func OpenDuckDB(path string) (*DuckDB, error) { +func OpenDuckDB(path string) (*DuckDB, core.Result) { conn, err := sql.Open("duckdb", path+"?access_mode=READ_ONLY") if err != nil { - return nil, core.E("store.OpenDuckDB", core.Sprintf("open duckdb %s", path), err) + return nil, core.Fail(core.E("store.OpenDuckDB", core.Sprintf("open duckdb %s", path), err)) } if err := conn.Ping(); err != nil { - _ = conn.Close() - return nil, core.E("store.OpenDuckDB", core.Sprintf("ping duckdb %s", path), err) + if closeErr := conn.Close(); closeErr != nil { + core.Error("duckdb close after failed ping", "err", closeErr) + } + return nil, core.Fail(core.E("store.OpenDuckDB", core.Sprintf("ping duckdb %s", path), err)) } - return &DuckDB{conn: conn, path: path}, nil + return &DuckDB{conn: conn, path: path}, core.Ok(nil) } // OpenDuckDBReadWrite opens a DuckDB database in read-write mode. @@ -68,16 +70,18 @@ func OpenDuckDB(path string) (*DuckDB, error) { // Usage example: // // db, err := store.OpenDuckDBReadWrite("/Volumes/Data/lem/lem.duckdb") -func OpenDuckDBReadWrite(path string) (*DuckDB, error) { +func OpenDuckDBReadWrite(path string) (*DuckDB, core.Result) { conn, err := sql.Open("duckdb", path) if err != nil { - return nil, core.E("store.OpenDuckDBReadWrite", core.Sprintf("open duckdb %s", path), err) + return nil, core.Fail(core.E("store.OpenDuckDBReadWrite", core.Sprintf("open duckdb %s", path), err)) } if err := conn.Ping(); err != nil { - _ = conn.Close() - return nil, core.E("store.OpenDuckDBReadWrite", core.Sprintf("ping duckdb %s", path), err) + if closeErr := conn.Close(); closeErr != nil { + core.Error("duckdb close after failed ping", "err", closeErr) + } + return nil, core.Fail(core.E("store.OpenDuckDBReadWrite", core.Sprintf("ping duckdb %s", path), err)) } - return &DuckDB{conn: conn, path: path}, nil + return &DuckDB{conn: conn, path: path}, core.Ok(nil) } // Close closes the database connection. @@ -85,8 +89,11 @@ func OpenDuckDBReadWrite(path string) (*DuckDB, error) { // Usage example: // // defer func() { _ = db.Close() }() -func (db *DuckDB) Close() error { - return db.conn.Close() +func (db *DuckDB) Close() core.Result { + if err := db.conn.Close(); err != nil { + return core.Fail(err) + } + return core.Ok(nil) } // Path returns the database file path. @@ -114,12 +121,12 @@ func (db *DuckDB) Conn() *sql.DB { // Usage example: // // err := db.Exec("INSERT INTO golden_set VALUES (?, ?)", idx, prompt) -func (db *DuckDB) Exec(query string, args ...any) error { +func (db *DuckDB) Exec(query string, args ...any) core.Result { _, err := db.conn.Exec(query, args...) if err != nil { - return core.E("store.DuckDB.Exec", "execute query", err) + return core.Fail(core.E("store.DuckDB.Exec", "execute query", err)) } - return nil + return core.Ok(nil) } // QueryRowScan executes a query expected to return at most one row and scans @@ -129,8 +136,11 @@ func (db *DuckDB) Exec(query string, args ...any) error { // // var count int // err := db.QueryRowScan("SELECT COUNT(*) FROM golden_set", &count) -func (db *DuckDB) QueryRowScan(query string, dest any, args ...any) error { - return db.conn.QueryRow(query, args...).Scan(dest) +func (db *DuckDB) QueryRowScan(query string, dest any, args ...any) core.Result { + if err := db.conn.QueryRow(query, args...).Scan(dest); err != nil { + return core.Fail(err) + } + return core.Ok(nil) } // GoldenSetRow represents one row from the golden_set table. @@ -273,14 +283,14 @@ type ExpansionPromptRow struct { // Usage example: // // rows, err := db.QueryGoldenSet(500) -func (db *DuckDB) QueryGoldenSet(minChars int) ([]GoldenSetRow, error) { +func (db *DuckDB) QueryGoldenSet(minChars int) ([]GoldenSetRow, core.Result) { rows, err := db.conn.Query( "SELECT idx, seed_id, domain, voice, prompt, response, gen_time, char_count "+ "FROM golden_set WHERE char_count >= ? ORDER BY idx", minChars, ) if err != nil { - return nil, core.E("store.DuckDB.QueryGoldenSet", "query golden_set", err) + return nil, core.Fail(core.E("store.DuckDB.QueryGoldenSet", "query golden_set", err)) } defer func() { _ = rows.Close() }() @@ -289,11 +299,14 @@ func (db *DuckDB) QueryGoldenSet(minChars int) ([]GoldenSetRow, error) { var r GoldenSetRow if err := rows.Scan(&r.Idx, &r.SeedID, &r.Domain, &r.Voice, &r.Prompt, &r.Response, &r.GenTime, &r.CharCount); err != nil { - return nil, core.E("store.DuckDB.QueryGoldenSet", "scan golden_set row", err) + return nil, core.Fail(core.E("store.DuckDB.QueryGoldenSet", "scan golden_set row", err)) } result = append(result, r) } - return result, rows.Err() + if err := rows.Err(); err != nil { + return nil, core.Fail(core.E("store.DuckDB.QueryGoldenSet", "iterate golden_set rows", err)) + } + return result, core.Ok(nil) } // CountGoldenSet returns the total count of golden set rows. @@ -301,13 +314,13 @@ func (db *DuckDB) QueryGoldenSet(minChars int) ([]GoldenSetRow, error) { // Usage example: // // count, err := db.CountGoldenSet() -func (db *DuckDB) CountGoldenSet() (int, error) { +func (db *DuckDB) CountGoldenSet() (int, core.Result) { var count int err := db.conn.QueryRow("SELECT COUNT(*) FROM golden_set").Scan(&count) if err != nil { - return 0, core.E("store.DuckDB.CountGoldenSet", "count golden_set", err) + return 0, core.Fail(core.E("store.DuckDB.CountGoldenSet", "count golden_set", err)) } - return count, nil + return count, core.Ok(nil) } // QueryExpansionPrompts returns expansion prompts filtered by status. @@ -315,7 +328,7 @@ func (db *DuckDB) CountGoldenSet() (int, error) { // Usage example: // // prompts, err := db.QueryExpansionPrompts("pending", 100) -func (db *DuckDB) QueryExpansionPrompts(status string, limit int) ([]ExpansionPromptRow, error) { +func (db *DuckDB) QueryExpansionPrompts(status string, limit int) ([]ExpansionPromptRow, core.Result) { query := "SELECT idx, seed_id, region, domain, language, prompt, prompt_en, priority, status " + "FROM expansion_prompts" var args []any @@ -332,7 +345,7 @@ func (db *DuckDB) QueryExpansionPrompts(status string, limit int) ([]ExpansionPr rows, err := db.conn.Query(query, args...) if err != nil { - return nil, core.E("store.DuckDB.QueryExpansionPrompts", "query expansion_prompts", err) + return nil, core.Fail(core.E("store.DuckDB.QueryExpansionPrompts", "query expansion_prompts", err)) } defer func() { _ = rows.Close() }() @@ -341,11 +354,14 @@ func (db *DuckDB) QueryExpansionPrompts(status string, limit int) ([]ExpansionPr var r ExpansionPromptRow if err := rows.Scan(&r.Idx, &r.SeedID, &r.Region, &r.Domain, &r.Language, &r.Prompt, &r.PromptEn, &r.Priority, &r.Status); err != nil { - return nil, core.E("store.DuckDB.QueryExpansionPrompts", "scan expansion_prompt row", err) + return nil, core.Fail(core.E("store.DuckDB.QueryExpansionPrompts", "scan expansion_prompt row", err)) } result = append(result, r) } - return result, rows.Err() + if err := rows.Err(); err != nil { + return nil, core.Fail(core.E("store.DuckDB.QueryExpansionPrompts", "iterate expansion_prompt rows", err)) + } + return result, core.Ok(nil) } // CountExpansionPrompts returns counts by status. @@ -353,16 +369,14 @@ func (db *DuckDB) QueryExpansionPrompts(status string, limit int) ([]ExpansionPr // Usage example: // // total, pending, err := db.CountExpansionPrompts() -func (db *DuckDB) CountExpansionPrompts() (total int, pending int, err error) { - err = db.conn.QueryRow("SELECT COUNT(*) FROM expansion_prompts").Scan(&total) - if err != nil { - return 0, 0, core.E("store.DuckDB.CountExpansionPrompts", "count expansion_prompts", err) +func (db *DuckDB) CountExpansionPrompts() (total int, pending int, err core.Result) { + if scanErr := db.conn.QueryRow("SELECT COUNT(*) FROM expansion_prompts").Scan(&total); scanErr != nil { + return 0, 0, core.Fail(core.E("store.DuckDB.CountExpansionPrompts", "count expansion_prompts", scanErr)) } - err = db.conn.QueryRow("SELECT COUNT(*) FROM expansion_prompts WHERE status = 'pending'").Scan(&pending) - if err != nil { - return total, 0, core.E("store.DuckDB.CountExpansionPrompts", "count pending expansion_prompts", err) + if scanErr := db.conn.QueryRow("SELECT COUNT(*) FROM expansion_prompts WHERE status = 'pending'").Scan(&pending); scanErr != nil { + return total, 0, core.Fail(core.E("store.DuckDB.CountExpansionPrompts", "count pending expansion_prompts", scanErr)) } - return total, pending, nil + return total, pending, core.Ok(nil) } // UpdateExpansionStatus updates the status of an expansion prompt by idx. @@ -370,12 +384,12 @@ func (db *DuckDB) CountExpansionPrompts() (total int, pending int, err error) { // Usage example: // // err := db.UpdateExpansionStatus(42, "done") -func (db *DuckDB) UpdateExpansionStatus(idx int64, status string) error { +func (db *DuckDB) UpdateExpansionStatus(idx int64, status string) core.Result { _, err := db.conn.Exec("UPDATE expansion_prompts SET status = ? WHERE idx = ?", status, idx) if err != nil { - return core.E("store.DuckDB.UpdateExpansionStatus", core.Sprintf("update expansion_prompt %d", idx), err) + return core.Fail(core.E("store.DuckDB.UpdateExpansionStatus", core.Sprintf("update expansion_prompt %d", idx), err)) } - return nil + return core.Ok(nil) } // QueryRows executes an arbitrary SQL query and returns results as maps. @@ -383,16 +397,16 @@ func (db *DuckDB) UpdateExpansionStatus(idx int64, status string) error { // Usage example: // // rows, err := db.QueryRows("SELECT COUNT(*) AS n FROM golden_set") -func (db *DuckDB) QueryRows(query string, args ...any) ([]map[string]any, error) { +func (db *DuckDB) QueryRows(query string, args ...any) ([]map[string]any, core.Result) { rows, err := db.conn.Query(query, args...) if err != nil { - return nil, core.E(opDuckDBQueryRows, "query", err) + return nil, core.Fail(core.E(opDuckDBQueryRows, "query", err)) } defer func() { _ = rows.Close() }() cols, err := rows.Columns() if err != nil { - return nil, core.E(opDuckDBQueryRows, "columns", err) + return nil, core.Fail(core.E(opDuckDBQueryRows, "columns", err)) } var result []map[string]any @@ -403,7 +417,7 @@ func (db *DuckDB) QueryRows(query string, args ...any) ([]map[string]any, error) ptrs[i] = &values[i] } if err := rows.Scan(ptrs...); err != nil { - return nil, core.E(opDuckDBQueryRows, "scan", err) + return nil, core.Fail(core.E(opDuckDBQueryRows, "scan", err)) } row := make(map[string]any, len(cols)) for i, col := range cols { @@ -411,7 +425,10 @@ func (db *DuckDB) QueryRows(query string, args ...any) ([]map[string]any, error) } result = append(result, row) } - return result, rows.Err() + if err := rows.Err(); err != nil { + return nil, core.Fail(core.E(opDuckDBQueryRows, "iterate", err)) + } + return result, core.Ok(nil) } // EnsureScoringTables creates the scoring tables if they do not exist. @@ -419,14 +436,14 @@ func (db *DuckDB) QueryRows(query string, args ...any) ([]map[string]any, error) // Usage example: // // if err := db.EnsureScoringTables(); err != nil { return } -func (db *DuckDB) EnsureScoringTables() error { +func (db *DuckDB) EnsureScoringTables() core.Result { if _, err := db.conn.Exec(core.Sprintf(`CREATE TABLE IF NOT EXISTS %s ( model TEXT, run_id TEXT, label TEXT, iteration INTEGER, correct INTEGER, total INTEGER, accuracy DOUBLE, scored_at TIMESTAMP DEFAULT current_timestamp, PRIMARY KEY (run_id, label) )`, TableCheckpointScores)); err != nil { - return core.E(opDuckDBEnsureScoringTables, "create checkpoint_scores", err) + return core.Fail(core.E(opDuckDBEnsureScoringTables, "create checkpoint_scores", err)) } if _, err := db.conn.Exec(core.Sprintf(`CREATE TABLE IF NOT EXISTS %s ( model TEXT, run_id TEXT, label TEXT, probe_id TEXT, @@ -434,16 +451,16 @@ func (db *DuckDB) EnsureScoringTables() error { scored_at TIMESTAMP DEFAULT current_timestamp, PRIMARY KEY (run_id, label, probe_id) )`, TableProbeResults)); err != nil { - return core.E(opDuckDBEnsureScoringTables, "create probe_results", err) + return core.Fail(core.E(opDuckDBEnsureScoringTables, "create probe_results", err)) } if _, err := db.conn.Exec(`CREATE TABLE IF NOT EXISTS scoring_results ( model TEXT, prompt_id TEXT, suite TEXT, dimension TEXT, score DOUBLE, scored_at TIMESTAMP DEFAULT current_timestamp )`); err != nil { - return core.E(opDuckDBEnsureScoringTables, "create scoring_results", err) + return core.Fail(core.E(opDuckDBEnsureScoringTables, "create scoring_results", err)) } - return nil + return core.Ok(nil) } // WriteScoringResult writes a single scoring dimension result to DuckDB. @@ -451,15 +468,15 @@ func (db *DuckDB) EnsureScoringTables() error { // Usage example: // // err := db.WriteScoringResult("lem-8b", "p-001", "ethics", "honesty", 0.95) -func (db *DuckDB) WriteScoringResult(model, promptID, suite, dimension string, score float64) error { +func (db *DuckDB) WriteScoringResult(model, promptID, suite, dimension string, score float64) core.Result { _, err := db.conn.Exec( `INSERT INTO scoring_results (model, prompt_id, suite, dimension, score) VALUES (?, ?, ?, ?, ?)`, model, promptID, suite, dimension, score, ) if err != nil { - return core.E("store.DuckDB.WriteScoringResult", "insert scoring result", err) + return core.Fail(core.E("store.DuckDB.WriteScoringResult", "insert scoring result", err)) } - return nil + return core.Ok(nil) } // TableCounts returns row counts for all known tables. @@ -468,7 +485,7 @@ func (db *DuckDB) WriteScoringResult(model, promptID, suite, dimension string, s // // counts, err := db.TableCounts() // n := counts["golden_set"] -func (db *DuckDB) TableCounts() (map[string]int, error) { +func (db *DuckDB) TableCounts() (map[string]int, core.Result) { tables := []string{"golden_set", "expansion_prompts", "seeds", "prompts", "training_examples", "gemini_responses", "benchmark_questions", "benchmark_results", "validations", TableCheckpointScores, TableProbeResults, "scoring_results"} @@ -482,5 +499,5 @@ func (db *DuckDB) TableCounts() (map[string]int, error) { } counts[t] = count } - return counts, nil + return counts, core.Ok(nil) } diff --git a/go/duckdb_example_test.go b/go/duckdb_example_test.go new file mode 100644 index 0000000..05d6be7 --- /dev/null +++ b/go/duckdb_example_test.go @@ -0,0 +1,128 @@ +package store + +import core "dappco.re/go" + +func exampleDuckDB() *DuckDB { + database, result := OpenDuckDBReadWrite("example.duckdb") + exampleRequireOK(result) + return database +} + +func ExampleOpenDuckDB() { + database, result := OpenDuckDB("example.duckdb") + if result.OK { + defer exampleRequireOK(database.Close()) + } + core.Println(result.OK) +} + +func ExampleOpenDuckDBReadWrite() { + database, result := OpenDuckDBReadWrite("example.duckdb") + exampleRequireOK(result) + defer exampleRequireOK(database.Close()) + core.Println(database.Path()) +} + +func ExampleDuckDB_Close() { + database := exampleDuckDB() + result := database.Close() + exampleRequireOK(result) +} + +func ExampleDuckDB_Path() { + database := exampleDuckDB() + defer exampleRequireOK(database.Close()) + core.Println(database.Path()) +} + +func ExampleDuckDB_Conn() { + database := exampleDuckDB() + defer exampleRequireOK(database.Close()) + core.Println(database.Conn() != nil) +} + +func ExampleDuckDB_Exec() { + database := exampleDuckDB() + defer exampleRequireOK(database.Close()) + result := database.Exec("CREATE TABLE example_items (id INTEGER)") + exampleRequireOK(result) +} + +func ExampleDuckDB_QueryRowScan() { + database := exampleDuckDB() + defer exampleRequireOK(database.Close()) + var value int + result := database.QueryRowScan("SELECT 1", &value) + exampleRequireOK(result) + core.Println(value) +} + +func ExampleDuckDB_QueryGoldenSet() { + database := exampleDuckDB() + defer exampleRequireOK(database.Close()) + rows, result := database.QueryGoldenSet(10) + exampleRequireOK(result) + core.Println(len(rows)) +} + +func ExampleDuckDB_CountGoldenSet() { + database := exampleDuckDB() + defer exampleRequireOK(database.Close()) + count, result := database.CountGoldenSet() + exampleRequireOK(result) + core.Println(count) +} + +func ExampleDuckDB_QueryExpansionPrompts() { + database := exampleDuckDB() + defer exampleRequireOK(database.Close()) + rows, result := database.QueryExpansionPrompts("pending", 10) + exampleRequireOK(result) + core.Println(len(rows)) +} + +func ExampleDuckDB_CountExpansionPrompts() { + database := exampleDuckDB() + defer exampleRequireOK(database.Close()) + total, pending, result := database.CountExpansionPrompts() + exampleRequireOK(result) + core.Println(total, pending) +} + +func ExampleDuckDB_UpdateExpansionStatus() { + database := exampleDuckDB() + defer exampleRequireOK(database.Close()) + result := database.UpdateExpansionStatus(7, "done") + exampleRequireOK(result) +} + +func ExampleDuckDB_QueryRows() { + database := exampleDuckDB() + defer exampleRequireOK(database.Close()) + rows, result := database.QueryRows("SELECT 1 AS value") + exampleRequireOK(result) + core.Println(rows) +} + +func ExampleDuckDB_EnsureScoringTables() { + database := exampleDuckDB() + defer exampleRequireOK(database.Close()) + result := database.EnsureScoringTables() + exampleRequireOK(result) +} + +func ExampleDuckDB_WriteScoringResult() { + database := exampleDuckDB() + defer exampleRequireOK(database.Close()) + exampleRequireOK(database.EnsureScoringTables()) + result := database.WriteScoringResult("model", "prompt-1", "suite", "helpfulness", 0.9) + exampleRequireOK(result) +} + +func ExampleDuckDB_TableCounts() { + database := exampleDuckDB() + defer exampleRequireOK(database.Close()) + counts, result := database.TableCounts() + exampleRequireOK(result) + core.Println(counts) +} diff --git a/duckdb_test.go b/go/duckdb_test.go similarity index 84% rename from duckdb_test.go rename to go/duckdb_test.go index 12cfd9d..911e9e1 100644 --- a/duckdb_test.go +++ b/go/duckdb_test.go @@ -1,7 +1,6 @@ package store_test import ( - . "dappco.re/go" store "dappco.re/go/store" ) @@ -56,7 +55,7 @@ func TestDuckdb_OpenDuckDB_Ugly(t *T) { } func TestDuckdb_DuckDB_Close_Good(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) path := database.Path() err := database.Close() AssertNoError(t, err) @@ -64,27 +63,27 @@ func TestDuckdb_DuckDB_Close_Good(t *T) { } func TestDuckdb_DuckDB_Close_Bad(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) RequireNoError(t, database.Close()) err := database.Exec("SELECT 1") AssertError(t, err) } func TestDuckdb_DuckDB_Close_Ugly(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) RequireNoError(t, database.Close()) err := database.Close() AssertNoError(t, err) } func TestDuckdb_DuckDB_Path_Good(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) path := database.Path() AssertContains(t, path, "ax7.duckdb") } func TestDuckdb_DuckDB_Path_Bad(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) RequireNoError(t, database.Close()) path := database.Path() AssertContains(t, path, "ax7.duckdb") @@ -99,45 +98,45 @@ func TestDuckdb_DuckDB_Path_Ugly(t *T) { } func TestDuckdb_DuckDB_Conn_Good(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) conn := database.Conn() AssertNotNil(t, conn) } func TestDuckdb_DuckDB_Conn_Bad(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) RequireNoError(t, database.Close()) conn := database.Conn() AssertNotNil(t, conn) } func TestDuckdb_DuckDB_Conn_Ugly(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) err := database.Conn().Ping() AssertNoError(t, err) } func TestDuckdb_DuckDB_Exec_Good(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) err := database.Exec("CREATE TABLE ax7_exec (id INTEGER)") AssertNoError(t, err) } func TestDuckdb_DuckDB_Exec_Bad(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) err := database.Exec("CREATE TABLE") AssertError(t, err) } func TestDuckdb_DuckDB_Exec_Ugly(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) RequireNoError(t, database.Exec("CREATE TABLE ax7_exec (id INTEGER)")) err := database.Exec("INSERT INTO ax7_exec VALUES (?)", 1) AssertNoError(t, err) } func TestDuckdb_DuckDB_QueryRowScan_Good(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) var n int err := database.QueryRowScan("SELECT 42", &n) AssertNoError(t, err) @@ -145,14 +144,14 @@ func TestDuckdb_DuckDB_QueryRowScan_Good(t *T) { } func TestDuckdb_DuckDB_QueryRowScan_Bad(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) var n int err := database.QueryRowScan("SELECT * FROM missing", &n) AssertError(t, err) } func TestDuckdb_DuckDB_QueryRowScan_Ugly(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) var text string err := database.QueryRowScan("SELECT 'agent'", &text) AssertNoError(t, err) @@ -160,45 +159,45 @@ func TestDuckdb_DuckDB_QueryRowScan_Ugly(t *T) { } func TestDuckdb_DuckDB_QueryGoldenSet_Good(t *T) { - database := ax7DuckDB(t) - ax7SeedDuckDB(t, database) + database := fixtureDuckDB(t) + fixtureSeedDuckDB(t, database) rows, err := database.QueryGoldenSet(1) AssertNoError(t, err) AssertEqual(t, "seed-1", rows[0].SeedID) } func TestDuckdb_DuckDB_QueryGoldenSet_Bad(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) rows, err := database.QueryGoldenSet(1) AssertError(t, err) AssertNil(t, rows) } func TestDuckdb_DuckDB_QueryGoldenSet_Ugly(t *T) { - database := ax7DuckDB(t) - ax7SeedDuckDB(t, database) + database := fixtureDuckDB(t) + fixtureSeedDuckDB(t, database) rows, err := database.QueryGoldenSet(1000) AssertNoError(t, err) AssertEmpty(t, rows) } func TestDuckdb_DuckDB_CountGoldenSet_Good(t *T) { - database := ax7DuckDB(t) - ax7SeedDuckDB(t, database) + database := fixtureDuckDB(t) + fixtureSeedDuckDB(t, database) count, err := database.CountGoldenSet() AssertNoError(t, err) AssertEqual(t, 1, count) } func TestDuckdb_DuckDB_CountGoldenSet_Bad(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) count, err := database.CountGoldenSet() AssertError(t, err) AssertEqual(t, 0, count) } func TestDuckdb_DuckDB_CountGoldenSet_Ugly(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) RequireNoError(t, database.Exec("CREATE TABLE golden_set (idx INTEGER)")) count, err := database.CountGoldenSet() AssertNoError(t, err) @@ -206,31 +205,31 @@ func TestDuckdb_DuckDB_CountGoldenSet_Ugly(t *T) { } func TestDuckdb_DuckDB_QueryExpansionPrompts_Good(t *T) { - database := ax7DuckDB(t) - ax7SeedDuckDB(t, database) + database := fixtureDuckDB(t) + fixtureSeedDuckDB(t, database) rows, err := database.QueryExpansionPrompts("pending", 10) AssertNoError(t, err) AssertEqual(t, "seed-7", rows[0].SeedID) } func TestDuckdb_DuckDB_QueryExpansionPrompts_Bad(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) rows, err := database.QueryExpansionPrompts("pending", 10) AssertError(t, err) AssertNil(t, rows) } func TestDuckdb_DuckDB_QueryExpansionPrompts_Ugly(t *T) { - database := ax7DuckDB(t) - ax7SeedDuckDB(t, database) + database := fixtureDuckDB(t) + fixtureSeedDuckDB(t, database) rows, err := database.QueryExpansionPrompts("done", 0) AssertNoError(t, err) AssertEmpty(t, rows) } func TestDuckdb_DuckDB_CountExpansionPrompts_Good(t *T) { - database := ax7DuckDB(t) - ax7SeedDuckDB(t, database) + database := fixtureDuckDB(t) + fixtureSeedDuckDB(t, database) total, pending, err := database.CountExpansionPrompts() AssertNoError(t, err) AssertEqual(t, 1, total) @@ -238,7 +237,7 @@ func TestDuckdb_DuckDB_CountExpansionPrompts_Good(t *T) { } func TestDuckdb_DuckDB_CountExpansionPrompts_Bad(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) total, pending, err := database.CountExpansionPrompts() AssertError(t, err) AssertEqual(t, 0, total) @@ -246,7 +245,7 @@ func TestDuckdb_DuckDB_CountExpansionPrompts_Bad(t *T) { } func TestDuckdb_DuckDB_CountExpansionPrompts_Ugly(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) RequireNoError(t, database.Exec("CREATE TABLE expansion_prompts (status VARCHAR)")) total, pending, err := database.CountExpansionPrompts() AssertNoError(t, err) @@ -255,8 +254,8 @@ func TestDuckdb_DuckDB_CountExpansionPrompts_Ugly(t *T) { } func TestDuckdb_DuckDB_UpdateExpansionStatus_Good(t *T) { - database := ax7DuckDB(t) - ax7SeedDuckDB(t, database) + database := fixtureDuckDB(t) + fixtureSeedDuckDB(t, database) err := database.UpdateExpansionStatus(7, "done") AssertNoError(t, err) rows, queryErr := database.QueryExpansionPrompts("done", 1) @@ -265,90 +264,90 @@ func TestDuckdb_DuckDB_UpdateExpansionStatus_Good(t *T) { } func TestDuckdb_DuckDB_UpdateExpansionStatus_Bad(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) err := database.UpdateExpansionStatus(7, "done") AssertError(t, err) } func TestDuckdb_DuckDB_UpdateExpansionStatus_Ugly(t *T) { - database := ax7DuckDB(t) - ax7SeedDuckDB(t, database) + database := fixtureDuckDB(t) + fixtureSeedDuckDB(t, database) err := database.UpdateExpansionStatus(999, "done") AssertNoError(t, err) } func TestDuckdb_DuckDB_QueryRows_Good(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) rows, err := database.QueryRows("SELECT 1 AS n") AssertNoError(t, err) AssertEqual(t, "1", Sprint(rows[0]["n"])) } func TestDuckdb_DuckDB_QueryRows_Bad(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) rows, err := database.QueryRows("SELECT * FROM missing") AssertError(t, err) AssertNil(t, rows) } func TestDuckdb_DuckDB_QueryRows_Ugly(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) rows, err := database.QueryRows("SELECT 'agent' AS name") AssertNoError(t, err) AssertEqual(t, "agent", rows[0]["name"]) } func TestDuckdb_DuckDB_EnsureScoringTables_Good(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) err := database.EnsureScoringTables() AssertNoError(t, err) AssertNotEmpty(t, database.Conn()) } func TestDuckdb_DuckDB_EnsureScoringTables_Bad(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) RequireNoError(t, database.Close()) err := database.EnsureScoringTables() AssertError(t, err) } func TestDuckdb_DuckDB_EnsureScoringTables_Ugly(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) RequireNoError(t, database.EnsureScoringTables()) err := database.EnsureScoringTables() AssertNoError(t, err) } func TestDuckdb_DuckDB_WriteScoringResult_Good(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) RequireNoError(t, database.EnsureScoringTables()) err := database.WriteScoringResult("model", "prompt", "suite", "dimension", 0.5) AssertNoError(t, err) } func TestDuckdb_DuckDB_WriteScoringResult_Bad(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) err := database.WriteScoringResult("model", "prompt", "suite", "dimension", 0.5) AssertError(t, err) } func TestDuckdb_DuckDB_WriteScoringResult_Ugly(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) RequireNoError(t, database.EnsureScoringTables()) err := database.WriteScoringResult("", "", "", "", 0) AssertNoError(t, err) } func TestDuckdb_DuckDB_TableCounts_Good(t *T) { - database := ax7DuckDB(t) - ax7SeedDuckDB(t, database) + database := fixtureDuckDB(t) + fixtureSeedDuckDB(t, database) counts, err := database.TableCounts() AssertNoError(t, err) AssertEqual(t, 1, counts["golden_set"]) } func TestDuckdb_DuckDB_TableCounts_Bad(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) RequireNoError(t, database.Close()) counts, err := database.TableCounts() AssertNoError(t, err) @@ -356,7 +355,7 @@ func TestDuckdb_DuckDB_TableCounts_Bad(t *T) { } func TestDuckdb_DuckDB_TableCounts_Ugly(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) RequireNoError(t, database.EnsureScoringTables()) counts, err := database.TableCounts() AssertNoError(t, err) diff --git a/events.go b/go/events.go similarity index 100% rename from events.go rename to go/events.go diff --git a/go/events_example_test.go b/go/events_example_test.go new file mode 100644 index 0000000..02f4365 --- /dev/null +++ b/go/events_example_test.go @@ -0,0 +1,31 @@ +package store + +import core "dappco.re/go" + +func ExampleEventType_String() { + label := EventDeleteGroup.String() + core.Println(label) +} + +func ExampleStore_Watch() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + events := storeInstance.Watch("config") + storeInstance.Unwatch("config", events) +} + +func ExampleStore_Unwatch() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + events := storeInstance.Watch("config") + storeInstance.Unwatch("config", events) +} + +func ExampleStore_OnChange() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + unregister := storeInstance.OnChange(func(event Event) { + core.Println(event.Type.String()) + }) + unregister() +} diff --git a/events_test.go b/go/events_test.go similarity index 97% rename from events_test.go rename to go/events_test.go index fdad19d..3d5fcf3 100644 --- a/events_test.go +++ b/go/events_test.go @@ -366,7 +366,7 @@ func TestEvents_EventType_String_Ugly(t *T) { } func TestEvents_Store_Watch_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) events := storeInstance.Watch("config") RequireNoError(t, storeInstance.Set("config", "colour", "blue")) event := <-events @@ -374,7 +374,7 @@ func TestEvents_Store_Watch_Good(t *T) { } func TestEvents_Store_Watch_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) events := storeInstance.Watch("config") _, ok := <-events @@ -382,7 +382,7 @@ func TestEvents_Store_Watch_Bad(t *T) { } func TestEvents_Store_Watch_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) events := storeInstance.Watch("*") RequireNoError(t, storeInstance.Set("config", "colour", "blue")) event := <-events @@ -390,7 +390,7 @@ func TestEvents_Store_Watch_Ugly(t *T) { } func TestEvents_Store_Unwatch_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) events := storeInstance.Watch("config") storeInstance.Unwatch("config", events) _, ok := <-events @@ -398,20 +398,20 @@ func TestEvents_Store_Unwatch_Good(t *T) { } func TestEvents_Store_Unwatch_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) AssertNotPanics(t, func() { storeInstance.Unwatch("config", nil) }) AssertFalse(t, storeInstance.IsClosed()) } func TestEvents_Store_Unwatch_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) events := storeInstance.Watch("config") storeInstance.Unwatch("config", events) AssertNotPanics(t, func() { storeInstance.Unwatch("config", events) }) } func TestEvents_Store_OnChange_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) called := false unregister := storeInstance.OnChange(func(event Event) { called = event.Group == "config" }) defer unregister() @@ -420,14 +420,14 @@ func TestEvents_Store_OnChange_Good(t *T) { } func TestEvents_Store_OnChange_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) unregister := storeInstance.OnChange(nil) unregister() AssertFalse(t, storeInstance.IsClosed()) } func TestEvents_Store_OnChange_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) count := 0 unregister := storeInstance.OnChange(func(Event) { count++ }) unregister() diff --git a/ax7_helpers_test.go b/go/fixture_helpers_test.go similarity index 69% rename from ax7_helpers_test.go rename to go/fixture_helpers_test.go index e5a436b..7c879f0 100644 --- a/ax7_helpers_test.go +++ b/go/fixture_helpers_test.go @@ -1,6 +1,10 @@ package store -import core "dappco.re/go" +import ( + "testing" + + core "dappco.re/go" +) type ( T = core.T @@ -18,12 +22,9 @@ var ( AssertContains = core.AssertContains AssertEmpty = core.AssertEmpty AssertEqual = core.AssertEqual - AssertError = core.AssertError - AssertErrorIs = core.AssertErrorIs AssertFalse = core.AssertFalse AssertLen = core.AssertLen AssertNil = core.AssertNil - AssertNoError = core.AssertNoError AssertNotEmpty = core.AssertNotEmpty AssertNotEqual = core.AssertNotEqual AssertNotNil = core.AssertNotNil @@ -34,12 +35,27 @@ var ( NewBuffer = core.NewBuffer Now = core.Now Path = core.Path - RequireNoError = core.RequireNoError RequireTrue = core.RequireTrue Sprint = core.Sprint ) -func ax7Store(t *T) *Store { +func AssertNoError(t testing.TB, value any) { + assertNoError(t, value) +} + +func RequireNoError(t testing.TB, value any) { + assertNoError(t, value) +} + +func AssertError(t testing.TB, value any) { + assertError(t, value) +} + +func AssertErrorIs(t testing.TB, value any, target error) { + assertErrorIs(t, value, target) +} + +func fixtureStore(t *T) *Store { t.Helper() storeInstance, err := New(testMemoryDatabasePath, WithPurgeInterval(24*Hour)) RequireNoError(t, err) @@ -47,7 +63,7 @@ func ax7Store(t *T) *Store { return storeInstance } -func ax7ConfiguredStore(t *T) (*Store, string) { +func fixtureConfiguredStore(t *T) (*Store, string) { t.Helper() stateDirectory := t.TempDir() storeInstance, err := NewConfigured(StoreConfig{ @@ -60,25 +76,25 @@ func ax7ConfiguredStore(t *T) (*Store, string) { return storeInstance, stateDirectory } -func ax7Workspace(t *T) (*Store, *Workspace) { +func fixtureWorkspace(t *T) (*Store, *Workspace) { t.Helper() - storeInstance, _ := ax7ConfiguredStore(t) - workspace, err := storeInstance.NewWorkspace(testAX7WorkspaceName) + storeInstance, _ := fixtureConfiguredStore(t) + workspace, err := storeInstance.NewWorkspace(testFixtureWorkspaceName) RequireNoError(t, err) t.Cleanup(func() { workspace.Discard() }) return storeInstance, workspace } -func ax7ScopedStore(t *T) *ScopedStore { +func fixtureScopedStore(t *T) *ScopedStore { t.Helper() - scopedStore, err := NewScopedConfigured(ax7Store(t), ScopedStoreConfig{Namespace: testTenantA}) + scopedStore, err := NewScopedConfigured(fixtureStore(t), ScopedStoreConfig{Namespace: testTenantA}) RequireNoError(t, err) return scopedStore } -func ax7QuotaScopedStore(t *T, maxKeys, maxGroups int) *ScopedStore { +func fixtureQuotaScopedStore(t *T, maxKeys, maxGroups int) *ScopedStore { t.Helper() - scopedStore, err := NewScopedConfigured(ax7Store(t), ScopedStoreConfig{ + scopedStore, err := NewScopedConfigured(fixtureStore(t), ScopedStoreConfig{ Namespace: testTenantA, Quota: QuotaConfig{MaxKeys: maxKeys, MaxGroups: maxGroups}, }) @@ -86,7 +102,7 @@ func ax7QuotaScopedStore(t *T, maxKeys, maxGroups int) *ScopedStore { return scopedStore } -func ax7DuckDB(t *T) *DuckDB { +func fixtureDuckDB(t *T) *DuckDB { t.Helper() path := Path(t.TempDir(), "ax7.duckdb") database, err := OpenDuckDBReadWrite(path) @@ -96,7 +112,7 @@ func ax7DuckDB(t *T) *DuckDB { } //nolint:unused // Compatibility helper kept for generated test lanes. -func ax7SeedDuckDB(t *T, database *DuckDB) { +func fixtureSeedDuckDB(t *T, database *DuckDB) { t.Helper() RequireNoError(t, database.Exec(`CREATE TABLE IF NOT EXISTS golden_set (idx INTEGER, seed_id VARCHAR, domain VARCHAR, voice VARCHAR, prompt VARCHAR, response VARCHAR, gen_time DOUBLE, char_count INTEGER)`)) RequireNoError(t, database.Exec(`DELETE FROM golden_set`)) @@ -106,7 +122,7 @@ func ax7SeedDuckDB(t *T, database *DuckDB) { RequireNoError(t, database.Exec(`INSERT INTO expansion_prompts VALUES (7, 'seed-7', 'western', 'ethics', 'en', 'prompt', 'prompt en', 1, 'pending')`)) } -func ax7CollectKeyValues(seq Seq2[KeyValue, error]) ([]KeyValue, error) { +func fixtureCollectKeyValues(seq Seq2[KeyValue, error]) ([]KeyValue, error) { var entries []KeyValue for entry, err := range seq { if err != nil { @@ -117,7 +133,7 @@ func ax7CollectKeyValues(seq Seq2[KeyValue, error]) ([]KeyValue, error) { return entries, nil } -func ax7CollectGroups(seq Seq2[string, error]) ([]string, error) { +func fixtureCollectGroups(seq Seq2[string, error]) ([]string, error) { var groups []string for group, err := range seq { if err != nil { @@ -128,7 +144,7 @@ func ax7CollectGroups(seq Seq2[string, error]) ([]string, error) { return groups, nil } -func ax7CollectStrings(seq Seq[string]) []string { +func fixtureCollectStrings(seq Seq[string]) []string { var values []string for value := range seq { values = append(values, value) @@ -136,53 +152,53 @@ func ax7CollectStrings(seq Seq[string]) []string { return values } -func ax7WriteFile(t *T, path, content string) { +func fixtureWriteFile(t *T, path, content string) { t.Helper() filesystem := (&core.Fs{}).NewUnrestricted() result := filesystem.Write(path, content) RequireTrue(t, result.OK) } -func newAX7Medium() *memoryMedium { +func newFixtureMedium() *memoryMedium { return newMemoryMedium() } -func ax7MustGet(t *T, storeInstance *Store, group, key string) string { +func fixtureMustGet(t *T, storeInstance *Store, group, key string) string { t.Helper() value, err := storeInstance.Get(group, key) RequireNoError(t, err) return value } -func ax7MustExists(t *T, storeInstance *Store, group, key string) bool { +func fixtureMustExists(t *T, storeInstance *Store, group, key string) bool { t.Helper() exists, err := storeInstance.Exists(group, key) RequireNoError(t, err) return exists } -func ax7MustGroupExists(t *T, storeInstance *Store, group string) bool { +func fixtureMustGroupExists(t *T, storeInstance *Store, group string) bool { t.Helper() exists, err := storeInstance.GroupExists(group) RequireNoError(t, err) return exists } -func ax7ScopedExists(t *T, scopedStore *ScopedStore, key string) bool { +func fixtureScopedExists(t *T, scopedStore *ScopedStore, key string) bool { t.Helper() exists, err := scopedStore.Exists(key) RequireNoError(t, err) return exists } -func ax7ScopedExistsIn(t *T, scopedStore *ScopedStore, group, key string) bool { +func fixtureScopedExistsIn(t *T, scopedStore *ScopedStore, group, key string) bool { t.Helper() exists, err := scopedStore.ExistsIn(group, key) RequireNoError(t, err) return exists } -func ax7ScopedGroupExists(t *T, scopedStore *ScopedStore, group string) bool { +func fixtureScopedGroupExists(t *T, scopedStore *ScopedStore, group string) bool { t.Helper() exists, err := scopedStore.GroupExists(group) RequireNoError(t, err) diff --git a/go.mod b/go/go.mod similarity index 100% rename from go.mod rename to go/go.mod diff --git a/go.sum b/go/go.sum similarity index 100% rename from go.sum rename to go/go.sum diff --git a/import.go b/go/import.go similarity index 62% rename from import.go rename to go/import.go index c89327b..6ce6a61 100644 --- a/import.go +++ b/go/import.go @@ -15,27 +15,27 @@ import ( var localFs = (&core.Fs{}).New("/") type duckDBImportSession interface { - exec(query string, args ...any) error - queryRowScan(query string, dest any, args ...any) error + exec(query string, args ...any) core.Result + queryRowScan(query string, dest any, args ...any) core.Result } type duckDBImportTransaction struct { transaction *sql.Tx } -func (session duckDBImportTransaction) exec(query string, args ...any) error { +func (session duckDBImportTransaction) exec(query string, args ...any) core.Result { _, err := session.transaction.Exec(query, args...) if err != nil { - return core.E("store.duckDBImportTransaction.Exec", "execute query", err) + return core.Fail(core.E("store.duckDBImportTransaction.Exec", "execute query", err)) } - return nil + return core.Ok(nil) } -func (session duckDBImportTransaction) queryRowScan(query string, dest any, args ...any) error { +func (session duckDBImportTransaction) queryRowScan(query string, dest any, args ...any) core.Result { if err := session.transaction.QueryRow(query, args...).Scan(dest); err != nil { - return core.E("store.duckDBImportTransaction.QueryRowScan", "scan row", err) + return core.Fail(core.E("store.duckDBImportTransaction.QueryRowScan", "scan row", err)) } - return nil + return core.Ok(nil) } // ScpFunc is a callback for executing SCP file transfers. @@ -101,19 +101,21 @@ type ImportConfig struct { // Usage example: // // err := store.ImportAll(db, store.ImportConfig{DataDir: "/Volumes/Data/lem"}, os.Stdout) -func ImportAll(db *DuckDB, cfg ImportConfig, w io.Writer) error { +func ImportAll(db *DuckDB, cfg ImportConfig, w io.Writer) core.Result { if db == nil || db.Conn() == nil { - return core.E(opImportAll, "database is nil", nil) + return core.Fail(core.E(opImportAll, "database is nil", nil)) } transaction, err := db.Conn().Begin() if err != nil { - return core.E(opImportAll, "begin import transaction", err) + return core.Fail(core.E(opImportAll, "begin import transaction", err)) } committed := false defer func() { if !committed { - _ = transaction.Rollback() + if rollbackErr := transaction.Rollback(); rollbackErr != nil { + core.Error("import transaction rollback failed", "err", rollbackErr) + } } }() @@ -126,25 +128,25 @@ func ImportAll(db *DuckDB, cfg ImportConfig, w io.Writer) error { session: duckDBImportTransaction{transaction: transaction}, } - for _, step := range []func() error{ + for _, step := range []func() core.Result{ run.importGoldenSet, run.importTrainingExamples, run.importBenchmarkResults, run.importBenchmarkQuestions, run.importSeeds, } { - if err := step(); err != nil { - return err + if result := step(); !result.OK { + return result } } if err := transaction.Commit(); err != nil { - return core.E(opImportAll, "commit import transaction", err) + return core.Fail(core.E(opImportAll, "commit import transaction", err)) } committed = true run.printSummary() - return nil + return core.Ok(nil) } type importAllRun struct { @@ -191,7 +193,7 @@ func normalisedImportM3Host(cfg ImportConfig) string { return "m3" } -func (run *importAllRun) importGoldenSet() error { +func (run *importAllRun) importGoldenSet() core.Result { goldenPath := core.JoinPath(run.cfg.DataDir, "gold-15k.jsonl") if !run.cfg.SkipM3 && run.cfg.Scp != nil { core.Print(run.writer, " Pulling golden set from M3...") @@ -201,12 +203,13 @@ func (run *importAllRun) importGoldenSet() error { } } if !isFile(goldenPath) { - return nil + return core.Ok(nil) } - if err := run.session.exec("DROP TABLE IF EXISTS golden_set"); err != nil { - return core.E(opImportAll, "drop golden_set", err) + if result := run.session.exec("DROP TABLE IF EXISTS golden_set"); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImportAll, "drop golden_set", err)) } - err := run.session.exec(core.Sprintf(` + result := run.session.exec(core.Sprintf(` CREATE TABLE golden_set AS SELECT idx::INT AS idx, @@ -220,26 +223,29 @@ func (run *importAllRun) importGoldenSet() error { length(response) - length(replace(response, ' ', '')) + 1 AS word_count FROM read_json_auto('%s', maximum_object_size=1048576) `, escapeSQLPath(goldenPath))) - if err != nil { - return core.E(opImportAll, "import golden_set", err) + if !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImportAll, "import golden_set", err)) } var n int - if err := run.session.queryRowScan("SELECT count(*) FROM golden_set", &n); err != nil { - return core.E(opImportAll, "count golden_set", err) + if result := run.session.queryRowScan("SELECT count(*) FROM golden_set", &n); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImportAll, "count golden_set", err)) } run.totals["golden_set"] = n core.Print(run.writer, " golden_set: %d rows", n) - return nil + return core.Ok(nil) } -func (run *importAllRun) importTrainingExamples() error { - if err := run.pullTrainingSets(); err != nil { - return err +func (run *importAllRun) importTrainingExamples() core.Result { + if result := run.pullTrainingSets(); !result.OK { + return result } - if err := run.session.exec("DROP TABLE IF EXISTS training_examples"); err != nil { - return core.E(opImportAll, "drop training_examples", err) + if result := run.session.exec("DROP TABLE IF EXISTS training_examples"); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImportAll, "drop training_examples", err)) } - if err := run.session.exec(` + if result := run.session.exec(` CREATE TABLE training_examples ( source VARCHAR, split VARCHAR, @@ -249,33 +255,34 @@ func (run *importAllRun) importTrainingExamples() error { full_messages TEXT, char_count INT ) - `); err != nil { - return core.E(opImportAll, "create training_examples", err) + `); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImportAll, "create training_examples", err)) } trainingTotal := 0 for _, trainingDir := range importTrainingDirs { - n, err := run.importTrainingDirectory(trainingDir) - if err != nil { - return err + n, result := run.importTrainingDirectory(trainingDir) + if !result.OK { + return result } trainingTotal += n } run.totals["training_examples"] = trainingTotal core.Print(run.writer, " training_examples: %d rows", trainingTotal) - return nil + return core.Ok(nil) } -func (run *importAllRun) pullTrainingSets() error { +func (run *importAllRun) pullTrainingSets() core.Result { if run.cfg.SkipM3 || run.cfg.Scp == nil { - return nil + return core.Ok(nil) } core.Print(run.writer, " Pulling training sets from M3...") for _, trainingDir := range importTrainingDirs { for _, relativePath := range trainingDir.files { localPath := core.JoinPath(run.cfg.DataDir, relativePath) if result := localFs.EnsureDir(core.PathDir(localPath)); !result.OK { - return core.E(opImportAll, "ensure training directory", result.Value.(error)) + return core.Fail(core.E(opImportAll, "ensure training directory", result.Value.(error))) } remote := core.Sprintf("%s:/Volumes/Data/lem/%s", run.m3Host, relativePath) if err := run.cfg.Scp(remote, localPath); err != nil { @@ -283,23 +290,24 @@ func (run *importAllRun) pullTrainingSets() error { } } } - return nil + return core.Ok(nil) } -func (run *importAllRun) importTrainingDirectory(trainingDir importTrainingDirectory) (int, error) { +func (run *importAllRun) importTrainingDirectory(trainingDir importTrainingDirectory) (int, core.Result) { total := 0 for _, relativePath := range trainingDir.files { localPath := core.JoinPath(run.cfg.DataDir, relativePath) if !isFile(localPath) { continue } - n, err := importTrainingFile(run.session, localPath, trainingDir.name, trainingSplit(relativePath)) - if err != nil { - return total, core.E(opImportAll, core.Sprintf("import training file %s", localPath), err) + n, result := importTrainingFile(run.session, localPath, trainingDir.name, trainingSplit(relativePath)) + if !result.OK { + err, _ := result.Value.(error) + return total, core.Fail(core.E(opImportAll, core.Sprintf("import training file %s", localPath), err)) } total += n } - return total, nil + return total, core.Ok(nil) } func trainingSplit(relativePath string) string { @@ -312,57 +320,59 @@ func trainingSplit(relativePath string) string { return "train" } -func (run *importAllRun) importBenchmarkResults() error { - benchLocal, err := run.ensureBenchmarkDirectory() - if err != nil { - return err +func (run *importAllRun) importBenchmarkResults() core.Result { + benchLocal, result := run.ensureBenchmarkDirectory() + if !result.OK { + return result } - if err := run.pullBenchmarks(benchLocal); err != nil { - return err + if result := run.pullBenchmarks(benchLocal); !result.OK { + return result } - if err := run.session.exec("DROP TABLE IF EXISTS benchmark_results"); err != nil { - return core.E(opImportAll, "drop benchmark_results", err) + if result := run.session.exec("DROP TABLE IF EXISTS benchmark_results"); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImportAll, "drop benchmark_results", err)) } - if err := run.session.exec(` + if result := run.session.exec(` CREATE TABLE benchmark_results ( source VARCHAR, id VARCHAR, benchmark VARCHAR, model VARCHAR, prompt TEXT, response TEXT, elapsed_seconds DOUBLE, domain VARCHAR ) - `); err != nil { - return core.E(opImportAll, "create benchmark_results", err) + `); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImportAll, "create benchmark_results", err)) } - benchTotal, err := run.importBenchmarkResultFiles(benchLocal) - if err != nil { - return err + benchTotal, result := run.importBenchmarkResultFiles(benchLocal) + if !result.OK { + return result } run.totals["benchmark_results"] = benchTotal core.Print(run.writer, " benchmark_results: %d rows", benchTotal) - return nil + return core.Ok(nil) } -func (run *importAllRun) ensureBenchmarkDirectory() (string, error) { +func (run *importAllRun) ensureBenchmarkDirectory() (string, core.Result) { benchLocal := core.JoinPath(run.cfg.DataDir, "benchmarks") if result := localFs.EnsureDir(benchLocal); !result.OK { - return "", core.E(opImportAll, core.Sprintf("ensure benchmark directory %s", benchLocal), result.Value.(error)) + return "", core.Fail(core.E(opImportAll, core.Sprintf("ensure benchmark directory %s", benchLocal), result.Value.(error))) } - return benchLocal, nil + return benchLocal, core.Ok(nil) } -func (run *importAllRun) pullBenchmarks(benchLocal string) error { +func (run *importAllRun) pullBenchmarks(benchLocal string) core.Result { if run.cfg.SkipM3 { - return nil + return core.Ok(nil) } core.Print(run.writer, " Pulling benchmarks from M3...") - if err := run.pullBenchmarkQuestionFiles(benchLocal); err != nil { - return err + if result := run.pullBenchmarkQuestionFiles(benchLocal); !result.OK { + return result } return run.pullBenchmarkResultDirectories(benchLocal) } -func (run *importAllRun) pullBenchmarkQuestionFiles(benchLocal string) error { +func (run *importAllRun) pullBenchmarkQuestionFiles(benchLocal string) core.Result { if run.cfg.Scp == nil { - return nil + return core.Ok(nil) } for _, benchmarkName := range benchmarkQuestionNames { remote := core.Sprintf("%s:/Volumes/Data/lem/benchmarks/%s.jsonl", run.m3Host, benchmarkName) @@ -370,97 +380,101 @@ func (run *importAllRun) pullBenchmarkQuestionFiles(benchLocal string) error { core.Print(run.writer, " WARNING: could not pull benchmark %s from M3: %v", benchmarkName, err) } } - return nil + return core.Ok(nil) } -func (run *importAllRun) pullBenchmarkResultDirectories(benchLocal string) error { +func (run *importAllRun) pullBenchmarkResultDirectories(benchLocal string) core.Result { if run.cfg.ScpDir == nil { - return nil + return core.Ok(nil) } for _, benchmarkSubdirectory := range benchmarkResultDirectories { localSubdirectory := core.JoinPath(benchLocal, benchmarkSubdirectory) if result := localFs.EnsureDir(localSubdirectory); !result.OK { - return core.E(opImportAll, core.Sprintf("ensure benchmark subdirectory %s", localSubdirectory), result.Value.(error)) + return core.Fail(core.E(opImportAll, core.Sprintf("ensure benchmark subdirectory %s", localSubdirectory), result.Value.(error))) } remote := core.Sprintf("%s:/Volumes/Data/lem/benchmarks/%s/", run.m3Host, benchmarkSubdirectory) if err := run.cfg.ScpDir(remote, localSubdirectory+"/"); err != nil { core.Print(run.writer, " WARNING: could not pull benchmark directory %s from M3: %v", benchmarkSubdirectory, err) } } - return nil + return core.Ok(nil) } -func (run *importAllRun) importBenchmarkResultFiles(benchLocal string) (int, error) { +func (run *importAllRun) importBenchmarkResultFiles(benchLocal string) (int, core.Result) { total := 0 for _, benchmarkSubdirectory := range benchmarkResultDirectories { - n, err := run.importBenchmarkResultDirectory(benchLocal, benchmarkSubdirectory) - if err != nil { - return total, err + n, result := run.importBenchmarkResultDirectory(benchLocal, benchmarkSubdirectory) + if !result.OK { + return total, result } total += n } - n, err := run.importStandaloneBenchmarkFiles(benchLocal) - if err != nil { - return total, err + n, result := run.importStandaloneBenchmarkFiles(benchLocal) + if !result.OK { + return total, result } - return total + n, nil + return total + n, core.Ok(nil) } -func (run *importAllRun) importBenchmarkResultDirectory(benchLocal, benchmarkSubdirectory string) (int, error) { +func (run *importAllRun) importBenchmarkResultDirectory(benchLocal, benchmarkSubdirectory string) (int, core.Result) { total := 0 resultDir := core.JoinPath(benchLocal, benchmarkSubdirectory) matches := core.PathGlob(core.JoinPath(resultDir, "*"+jsonlExtension)) for _, jsonFile := range matches { - n, err := importBenchmarkFile(run.session, jsonFile, benchmarkSubdirectory) - if err != nil { - return total, core.E(opImportAll, core.Sprintf("import benchmark file %s", jsonFile), err) + n, result := importBenchmarkFile(run.session, jsonFile, benchmarkSubdirectory) + if !result.OK { + err, _ := result.Value.(error) + return total, core.Fail(core.E(opImportAll, core.Sprintf("import benchmark file %s", jsonFile), err)) } total += n } - return total, nil + return total, core.Ok(nil) } -func (run *importAllRun) importStandaloneBenchmarkFiles(benchLocal string) (int, error) { +func (run *importAllRun) importStandaloneBenchmarkFiles(benchLocal string) (int, core.Result) { total := 0 for _, benchmarkFile := range standaloneBenchmarkFileNames { localPath := core.JoinPath(benchLocal, benchmarkFile+jsonlExtension) - if err := run.pullStandaloneBenchmarkFile(localPath, benchmarkFile); err != nil { - return total, err + if result := run.pullStandaloneBenchmarkFile(localPath, benchmarkFile); !result.OK { + return total, result } if !isFile(localPath) { continue } - n, err := importBenchmarkFile(run.session, localPath, "benchmark") - if err != nil { - return total, core.E(opImportAll, core.Sprintf("import benchmark file %s", localPath), err) + n, result := importBenchmarkFile(run.session, localPath, "benchmark") + if !result.OK { + err, _ := result.Value.(error) + return total, core.Fail(core.E(opImportAll, core.Sprintf("import benchmark file %s", localPath), err)) } total += n } - return total, nil + return total, core.Ok(nil) } -func (run *importAllRun) pullStandaloneBenchmarkFile(localPath, benchmarkFile string) error { +func (run *importAllRun) pullStandaloneBenchmarkFile(localPath, benchmarkFile string) core.Result { if isFile(localPath) || run.cfg.SkipM3 || run.cfg.Scp == nil { - return nil + return core.Ok(nil) } remote := core.Sprintf("%s:/Volumes/Data/lem/benchmarks/%s.jsonl", run.m3Host, benchmarkFile) if err := run.cfg.Scp(remote, localPath); err != nil { core.Print(run.writer, " WARNING: could not pull benchmark file %s from M3: %v", benchmarkFile, err) } - return nil + return core.Ok(nil) } -func (run *importAllRun) importBenchmarkQuestions() error { - if err := run.session.exec("DROP TABLE IF EXISTS benchmark_questions"); err != nil { - return core.E(opImportAll, "drop benchmark_questions", err) +func (run *importAllRun) importBenchmarkQuestions() core.Result { + if result := run.session.exec("DROP TABLE IF EXISTS benchmark_questions"); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImportAll, "drop benchmark_questions", err)) } - if err := run.session.exec(` + if result := run.session.exec(` CREATE TABLE benchmark_questions ( benchmark VARCHAR, id VARCHAR, question TEXT, best_answer TEXT, correct_answers TEXT, incorrect_answers TEXT, category VARCHAR ) - `); err != nil { - return core.E(opImportAll, "create benchmark_questions", err) + `); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImportAll, "create benchmark_questions", err)) } benchLocal := core.JoinPath(run.cfg.DataDir, "benchmarks") @@ -470,27 +484,30 @@ func (run *importAllRun) importBenchmarkQuestions() error { if !isFile(localPath) { continue } - n, err := importBenchmarkQuestions(run.session, localPath, benchmarkName) - if err != nil { - return core.E(opImportAll, core.Sprintf("import benchmark questions %s", localPath), err) + n, result := importBenchmarkQuestions(run.session, localPath, benchmarkName) + if !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImportAll, core.Sprintf("import benchmark questions %s", localPath), err)) } benchQTotal += n } run.totals["benchmark_questions"] = benchQTotal core.Print(run.writer, " benchmark_questions: %d rows", benchQTotal) - return nil + return core.Ok(nil) } -func (run *importAllRun) importSeeds() error { - if err := run.session.exec("DROP TABLE IF EXISTS seeds"); err != nil { - return core.E(opImportAll, "drop seeds", err) +func (run *importAllRun) importSeeds() core.Result { + if result := run.session.exec("DROP TABLE IF EXISTS seeds"); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImportAll, "drop seeds", err)) } - if err := run.session.exec(` + if result := run.session.exec(` CREATE TABLE seeds ( source_file VARCHAR, region VARCHAR, seed_id VARCHAR, domain VARCHAR, prompt TEXT ) - `); err != nil { - return core.E(opImportAll, "create seeds", err) + `); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImportAll, "create seeds", err)) } seedTotal := 0 @@ -498,15 +515,16 @@ func (run *importAllRun) importSeeds() error { if !isDir(seedDir) { continue } - n, err := importSeeds(run.session, seedDir) - if err != nil { - return core.E(opImportAll, core.Sprintf("import seeds %s", seedDir), err) + n, result := importSeeds(run.session, seedDir) + if !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImportAll, core.Sprintf("import seeds %s", seedDir), err)) } seedTotal += n } run.totals["seeds"] = seedTotal core.Print(run.writer, " seeds: %d rows", seedTotal) - return nil + return core.Ok(nil) } func (run *importAllRun) printSummary() { @@ -523,10 +541,10 @@ func (run *importAllRun) printSummary() { core.Print(run.writer, "\nDatabase: %s", run.db.Path()) } -func importTrainingFile(db duckDBImportSession, path, source, split string) (int, error) { +func importTrainingFile(db duckDBImportSession, path, source, split string) (int, core.Result) { r := localFs.Open(path) if !r.OK { - return 0, core.E(opImportTrainingFile, core.Sprintf(openPathFormat, path), r.Value.(error)) + return 0, core.Fail(core.E(opImportTrainingFile, core.Sprintf(openPathFormat, path), r.Value.(error))) } f := r.Value.(io.ReadCloser) defer func() { _ = f.Close() }() @@ -538,19 +556,21 @@ func importTrainingFile(db duckDBImportSession, path, source, split string) (int lineNumber := 0 for scanner.Scan() { lineNumber++ - example, err := trainingExampleFromJSON(scanner.Bytes()) - if err != nil { - return count, core.E(opImportTrainingFile, core.Sprintf(parsePathLineFormat, path, lineNumber), err) + example, result := trainingExampleFromJSON(scanner.Bytes()) + if !result.OK { + err, _ := result.Value.(error) + return count, core.Fail(core.E(opImportTrainingFile, core.Sprintf(parsePathLineFormat, path, lineNumber), err)) } - if err := insertTrainingExample(db, source, split, example); err != nil { - return count, core.E(opImportTrainingFile, "insert training example", err) + if result := insertTrainingExample(db, source, split, example); !result.OK { + err, _ := result.Value.(error) + return count, core.Fail(core.E(opImportTrainingFile, "insert training example", err)) } count++ } if err := scanner.Err(); err != nil { - return count, core.E(opImportTrainingFile, "scan training file", err) + return count, core.Fail(core.E(opImportTrainingFile, "scan training file", err)) } - return count, nil + return count, core.Ok(nil) } type trainingExample struct { @@ -560,15 +580,15 @@ type trainingExample struct { assistantCount int } -func trainingExampleFromJSON(data []byte) (trainingExample, error) { +func trainingExampleFromJSON(data []byte) (trainingExample, core.Result) { var rec struct { Messages []ChatMessage `json:"messages"` } if r := core.JSONUnmarshal(data, &rec); !r.OK { parseErr, _ := r.Value.(error) - return trainingExample{}, parseErr + return trainingExample{}, core.Fail(parseErr) } - return trainingExampleFromMessages(rec.Messages), nil + return trainingExampleFromMessages(rec.Messages), core.Ok(nil) } func trainingExampleFromMessages(messages []ChatMessage) trainingExample { @@ -587,7 +607,7 @@ func trainingExampleFromMessages(messages []ChatMessage) trainingExample { return example } -func insertTrainingExample(db duckDBImportSession, source, split string, example trainingExample) error { +func insertTrainingExample(db duckDBImportSession, source, split string, example trainingExample) core.Result { return db.exec( `INSERT INTO training_examples VALUES (?, ?, ?, ?, ?, ?, ?)`, source, @@ -600,10 +620,10 @@ func insertTrainingExample(db duckDBImportSession, source, split string, example ) } -func importBenchmarkFile(db duckDBImportSession, path, source string) (int, error) { +func importBenchmarkFile(db duckDBImportSession, path, source string) (int, core.Result) { r := localFs.Open(path) if !r.OK { - return 0, core.E(opImportBenchmarkFile, core.Sprintf(openPathFormat, path), r.Value.(error)) + return 0, core.Fail(core.E(opImportBenchmarkFile, core.Sprintf(openPathFormat, path), r.Value.(error))) } f := r.Value.(io.ReadCloser) defer func() { _ = f.Close() }() @@ -618,10 +638,10 @@ func importBenchmarkFile(db duckDBImportSession, path, source string) (int, erro var rec map[string]any if r := core.JSONUnmarshal(scanner.Bytes(), &rec); !r.OK { parseErr, _ := r.Value.(error) - return count, core.E(opImportBenchmarkFile, core.Sprintf(parsePathLineFormat, path, lineNumber), parseErr) + return count, core.Fail(core.E(opImportBenchmarkFile, core.Sprintf(parsePathLineFormat, path, lineNumber), parseErr)) } - if err := db.exec(`INSERT INTO benchmark_results VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + if result := db.exec(`INSERT INTO benchmark_results VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, source, core.Sprint(rec["id"]), strOrEmpty(rec, "benchmark"), @@ -630,21 +650,22 @@ func importBenchmarkFile(db duckDBImportSession, path, source string) (int, erro strOrEmpty(rec, "response"), floatOrZero(rec, "elapsed_seconds"), strOrEmpty(rec, "domain"), - ); err != nil { - return count, core.E(opImportBenchmarkFile, "insert benchmark result", err) + ); !result.OK { + err, _ := result.Value.(error) + return count, core.Fail(core.E(opImportBenchmarkFile, "insert benchmark result", err)) } count++ } if err := scanner.Err(); err != nil { - return count, core.E(opImportBenchmarkFile, "scan benchmark file", err) + return count, core.Fail(core.E(opImportBenchmarkFile, "scan benchmark file", err)) } - return count, nil + return count, core.Ok(nil) } -func importBenchmarkQuestions(db duckDBImportSession, path, benchmark string) (int, error) { +func importBenchmarkQuestions(db duckDBImportSession, path, benchmark string) (int, core.Result) { r := localFs.Open(path) if !r.OK { - return 0, core.E(opImportBenchmarkQuestions, core.Sprintf(openPathFormat, path), r.Value.(error)) + return 0, core.Fail(core.E(opImportBenchmarkQuestions, core.Sprintf(openPathFormat, path), r.Value.(error))) } f := r.Value.(io.ReadCloser) defer func() { _ = f.Close() }() @@ -659,13 +680,13 @@ func importBenchmarkQuestions(db duckDBImportSession, path, benchmark string) (i var rec map[string]any if r := core.JSONUnmarshal(scanner.Bytes(), &rec); !r.OK { parseErr, _ := r.Value.(error) - return count, core.E(opImportBenchmarkQuestions, core.Sprintf(parsePathLineFormat, path, lineNumber), parseErr) + return count, core.Fail(core.E(opImportBenchmarkQuestions, core.Sprintf(parsePathLineFormat, path, lineNumber), parseErr)) } correctJSON := core.JSONMarshalString(rec["correct_answers"]) incorrectJSON := core.JSONMarshalString(rec["incorrect_answers"]) - if err := db.exec(`INSERT INTO benchmark_questions VALUES (?, ?, ?, ?, ?, ?, ?)`, + if result := db.exec(`INSERT INTO benchmark_questions VALUES (?, ?, ?, ?, ?, ?, ?)`, benchmark, core.Sprint(rec["id"]), strOrEmpty(rec, "question"), @@ -673,66 +694,67 @@ func importBenchmarkQuestions(db duckDBImportSession, path, benchmark string) (i correctJSON, incorrectJSON, strOrEmpty(rec, "category"), - ); err != nil { - return count, core.E(opImportBenchmarkQuestions, "insert benchmark question", err) + ); !result.OK { + err, _ := result.Value.(error) + return count, core.Fail(core.E(opImportBenchmarkQuestions, "insert benchmark question", err)) } count++ } if err := scanner.Err(); err != nil { - return count, core.E(opImportBenchmarkQuestions, "scan benchmark questions", err) + return count, core.Fail(core.E(opImportBenchmarkQuestions, "scan benchmark questions", err)) } - return count, nil + return count, core.Ok(nil) } -func importSeeds(db duckDBImportSession, seedDir string) (int, error) { +func importSeeds(db duckDBImportSession, seedDir string) (int, core.Result) { count := 0 - if err := walkDir(seedDir, func(path string) error { - imported, err := importSeedFile(db, seedDir, path) - if err != nil { - return err + if result := walkDir(seedDir, func(path string) core.Result { + imported, result := importSeedFile(db, seedDir, path) + if !result.OK { + return result } count += imported - return nil - }); err != nil { - return count, err + return core.Ok(nil) + }); !result.OK { + return count, result } - return count, nil + return count, core.Ok(nil) } -func importSeedFile(db duckDBImportSession, seedDir, path string) (int, error) { +func importSeedFile(db duckDBImportSession, seedDir, path string) (int, core.Result) { if !core.HasSuffix(path, ".json") { - return 0, nil + return 0, core.Ok(nil) } rel := core.TrimPrefix(path, seedDir+"/") region := core.TrimSuffix(core.PathBase(path), ".json") - seedsList, err := readSeedList(path, rel) - if err != nil { - return 0, err + seedsList, result := readSeedList(path, rel) + if !result.OK { + return 0, result } count := 0 for _, seed := range seedsList { - inserted, err := insertSeed(db, rel, region, seed) - if err != nil { - return count, err + inserted, result := insertSeed(db, rel, region, seed) + if !result.OK { + return count, result } if inserted { count++ } } - return count, nil + return count, core.Ok(nil) } -func readSeedList(path, rel string) ([]any, error) { +func readSeedList(path, rel string) ([]any, core.Result) { readResult := localFs.Read(path) if !readResult.OK { - return nil, core.E(opImportSeeds, core.Sprintf("read seed file %s", rel), readResult.Value.(error)) + return nil, core.Fail(core.E(opImportSeeds, core.Sprintf("read seed file %s", rel), readResult.Value.(error))) } var raw any if r := core.JSONUnmarshal([]byte(readResult.Value.(string)), &raw); !r.OK { err, _ := r.Value.(error) - return nil, core.E(opImportSeeds, core.Sprintf("parse seed file %s", rel), err) + return nil, core.Fail(core.E(opImportSeeds, core.Sprintf("parse seed file %s", rel), err)) } - return seedListFromRaw(raw), nil + return seedListFromRaw(raw), core.Ok(nil) } func seedListFromRaw(raw any) []any { @@ -750,27 +772,29 @@ func seedListFromRaw(raw any) []any { return nil } -func insertSeed(db duckDBImportSession, rel, region string, seed any) (bool, error) { +func insertSeed(db duckDBImportSession, rel, region string, seed any) (bool, core.Result) { switch typedSeed := seed.(type) { case map[string]any: - if err := db.exec( + if result := db.exec( `INSERT INTO seeds VALUES (?, ?, ?, ?, ?)`, rel, region, strOrEmpty(typedSeed, "seed_id"), strOrEmpty(typedSeed, "domain"), seedPrompt(typedSeed), - ); err != nil { - return true, core.E(opImportSeeds, "insert seed prompt", err) + ); !result.OK { + err, _ := result.Value.(error) + return true, core.Fail(core.E(opImportSeeds, "insert seed prompt", err)) } - return true, nil + return true, core.Ok(nil) case string: - if err := db.exec(`INSERT INTO seeds VALUES (?, ?, ?, ?, ?)`, rel, region, "", "", typedSeed); err != nil { - return true, core.E(opImportSeeds, "insert seed string", err) + if result := db.exec(`INSERT INTO seeds VALUES (?, ?, ?, ?, ?)`, rel, region, "", "", typedSeed); !result.OK { + err, _ := result.Value.(error) + return true, core.Fail(core.E(opImportSeeds, "insert seed string", err)) } - return true, nil + return true, core.Ok(nil) default: - return false, nil + return false, core.Ok(nil) } } @@ -784,28 +808,28 @@ func seedPrompt(seed map[string]any) string { } // walkDir recursively visits all regular files under root, calling fn for each. -func walkDir(root string, fn func(path string) error) error { +func walkDir(root string, fn func(path string) core.Result) core.Result { r := localFs.List(root) if !r.OK { - return core.E("store.walkDir", core.Sprintf("list %s", root), r.Value.(error)) + return core.Fail(core.E("store.walkDir", core.Sprintf("list %s", root), r.Value.(error))) } entries, ok := r.Value.([]fs.DirEntry) if !ok { - return core.E("store.walkDir", core.Sprintf("list %s returned invalid entries", root), nil) + return core.Fail(core.E("store.walkDir", core.Sprintf("list %s returned invalid entries", root), nil)) } for _, entry := range entries { full := core.JoinPath(root, entry.Name()) if entry.IsDir() { - if err := walkDir(full, fn); err != nil { - return err + if result := walkDir(full, fn); !result.OK { + return result } } else { - if err := fn(full); err != nil { - return err + if result := fn(full); !result.OK { + return result } } } - return nil + return core.Ok(nil) } // strOrEmpty extracts a string value from a map, returning an empty string if diff --git a/go/import_example_test.go b/go/import_example_test.go new file mode 100644 index 0000000..4e24d5d --- /dev/null +++ b/go/import_example_test.go @@ -0,0 +1,12 @@ +package store + +import core "dappco.re/go" + +func ExampleImportAll() { + database, result := OpenDuckDBReadWrite("import.duckdb") + exampleRequireOK(result) + defer exampleRequireOK(database.Close()) + buffer := core.NewBuffer() + result = ImportAll(database, ImportConfig{DataDir: "data", SkipM3: true}, buffer) + core.Println(result.OK) +} diff --git a/import_export_test.go b/go/import_export_test.go similarity index 100% rename from import_export_test.go rename to go/import_export_test.go diff --git a/import_test.go b/go/import_test.go similarity index 93% rename from import_test.go rename to go/import_test.go index bed007e..a275536 100644 --- a/import_test.go +++ b/go/import_test.go @@ -10,13 +10,13 @@ type importSessionStub struct { inserts int } -func (session *importSessionStub) exec(string, ...any) error { +func (session *importSessionStub) exec(string, ...any) core.Result { session.inserts++ - return nil + return core.Ok(nil) } -func (session *importSessionStub) queryRowScan(string, any, ...any) error { - return nil +func (session *importSessionStub) queryRowScan(string, any, ...any) core.Result { + return core.Ok(nil) } func TestImport_ImportTrainingFile_Bad_MalformedJSONL(t *testing.T) { @@ -70,7 +70,7 @@ func TestImport_ImportSeeds_Bad_WalkFailure(t *testing.T) { } func TestImport_ImportAll_Good(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) output := NewBuffer() err := ImportAll(database, ImportConfig{DataDir: t.TempDir(), SkipM3: true}, output) AssertNoError(t, err) @@ -85,7 +85,7 @@ func TestImport_ImportAll_Bad(t *T) { } func TestImport_ImportAll_Ugly(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) output := NewBuffer() err := ImportAll(database, ImportConfig{DataDir: t.TempDir(), SkipM3: false, Scp: func(string, string) error { return NewError("offline") }}, output) AssertNoError(t, err) diff --git a/inventory.go b/go/inventory.go similarity index 87% rename from inventory.go rename to go/inventory.go index f8f0174..2e6064a 100644 --- a/inventory.go +++ b/go/inventory.go @@ -35,10 +35,11 @@ type duckDBTableDetail struct { // Usage example: // // err := store.PrintDuckDBInventory(db, os.Stdout) -func PrintDuckDBInventory(db *DuckDB, w io.Writer) error { - counts, err := db.TableCounts() - if err != nil { - return core.E("store.PrintDuckDBInventory", "table counts", err) +func PrintDuckDBInventory(db *DuckDB, w io.Writer) core.Result { + counts, result := db.TableCounts() + if !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E("store.PrintDuckDBInventory", "table counts", err)) } details := gatherDuckDBDetails(db, counts) @@ -64,7 +65,7 @@ func PrintDuckDBInventory(db *DuckDB, w io.Writer) error { core.Print(w, "%s", repeat("-", 52)) core.Print(w, " %-24s %8d rows", "TOTAL", grand) - return nil + return core.Ok(nil) } // gatherDuckDBDetails runs per-table detail queries and returns annotations @@ -105,8 +106,8 @@ func trainingExamplesDetail(db *DuckDB, counts map[string]int) *duckDBTableDetai if _, ok := counts["training_examples"]; !ok { return nil } - rows, err := db.QueryRows("SELECT COUNT(DISTINCT source) AS n FROM training_examples") - if err != nil || len(rows) == 0 { + rows, result := db.QueryRows("SELECT COUNT(DISTINCT source) AS n FROM training_examples") + if !result.OK || len(rows) == 0 { return nil } return &duckDBTableDetail{notes: []string{core.Sprintf("%d sources", duckDBToInt(rows[0]["n"]))}} @@ -126,8 +127,8 @@ func promptsDetail(db *DuckDB, counts map[string]int) *duckDBTableDetail { } func appendDistinctPromptCount(db *DuckDB, detail *duckDBTableDetail, column, label string) { - rows, err := db.QueryRows(core.Sprintf("SELECT COUNT(DISTINCT %s) AS n FROM prompts", column)) - if err == nil && len(rows) > 0 { + rows, result := db.QueryRows(core.Sprintf("SELECT COUNT(DISTINCT %s) AS n FROM prompts", column)) + if result.OK && len(rows) > 0 { detail.notes = append(detail.notes, core.Sprintf("%d %s", duckDBToInt(rows[0]["n"]), label)) } } @@ -136,10 +137,10 @@ func geminiResponsesDetail(db *DuckDB, counts map[string]int) *duckDBTableDetail if _, ok := counts["gemini_responses"]; !ok { return nil } - rows, err := db.QueryRows( + rows, result := db.QueryRows( "SELECT source_model, COUNT(*) AS n FROM gemini_responses GROUP BY source_model ORDER BY n DESC", ) - if err != nil || len(rows) == 0 { + if !result.OK || len(rows) == 0 { return nil } var parts []string @@ -159,8 +160,8 @@ func benchmarkResultsDetail(db *DuckDB, counts map[string]int) *duckDBTableDetai if _, ok := counts["benchmark_results"]; !ok { return nil } - rows, err := db.QueryRows("SELECT COUNT(DISTINCT source) AS n FROM benchmark_results") - if err != nil || len(rows) == 0 { + rows, result := db.QueryRows("SELECT COUNT(DISTINCT source) AS n FROM benchmark_results") + if !result.OK || len(rows) == 0 { return nil } return &duckDBTableDetail{notes: []string{core.Sprintf("%d categories", duckDBToInt(rows[0]["n"]))}} diff --git a/go/inventory_example_test.go b/go/inventory_example_test.go new file mode 100644 index 0000000..812cdad --- /dev/null +++ b/go/inventory_example_test.go @@ -0,0 +1,12 @@ +package store + +import core "dappco.re/go" + +func ExamplePrintDuckDBInventory() { + database := exampleDuckDB() + defer exampleRequireOK(database.Close()) + buffer := core.NewBuffer() + result := PrintDuckDBInventory(database, buffer) + exampleRequireOK(result) + core.Println(buffer.Len()) +} diff --git a/inventory_test.go b/go/inventory_test.go similarity index 86% rename from inventory_test.go rename to go/inventory_test.go index 39d2fb0..8d3de43 100644 --- a/inventory_test.go +++ b/go/inventory_test.go @@ -1,13 +1,12 @@ package store_test import ( - . "dappco.re/go" store "dappco.re/go/store" ) func TestInventory_PrintDuckDBInventory_Good(t *T) { - database := ax7DuckDB(t) - ax7SeedDuckDB(t, database) + database := fixtureDuckDB(t) + fixtureSeedDuckDB(t, database) output := NewBuffer() err := store.PrintDuckDBInventory(database, output) AssertNoError(t, err) @@ -15,7 +14,7 @@ func TestInventory_PrintDuckDBInventory_Good(t *T) { } func TestInventory_PrintDuckDBInventory_Bad(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) RequireNoError(t, database.Close()) output := NewBuffer() err := store.PrintDuckDBInventory(database, output) @@ -24,7 +23,7 @@ func TestInventory_PrintDuckDBInventory_Bad(t *T) { } func TestInventory_PrintDuckDBInventory_Ugly(t *T) { - database := ax7DuckDB(t) + database := fixtureDuckDB(t) RequireNoError(t, database.EnsureScoringTables()) output := NewBuffer() err := store.PrintDuckDBInventory(database, output) diff --git a/journal.go b/go/journal.go similarity index 78% rename from journal.go rename to go/journal.go index 2ed18bb..bc92d5a 100644 --- a/journal.go +++ b/go/journal.go @@ -57,8 +57,8 @@ type journalExecutor interface { // Workspace.Commit uses this same journal write path before it updates the // summary row in `workspace:NAME`. func (storeInstance *Store) CommitToJournal(measurement string, fields map[string]any, tags map[string]string) core.Result { - if err := storeInstance.ensureReady(opCommitToJournal); err != nil { - return core.Fail(err) + if result := storeInstance.ensureReady(opCommitToJournal); !result.OK { + return result } if measurement == "" { return core.Fail(core.E(opCommitToJournal, "measurement is empty", nil)) @@ -69,28 +69,30 @@ func (storeInstance *Store) CommitToJournal(measurement string, fields map[strin if tags == nil { tags = map[string]string{} } - if err := ensureJournalSchema(storeInstance.sqliteDatabase); err != nil { + if result := ensureJournalSchema(storeInstance.sqliteDatabase); !result.OK { + err, _ := result.Value.(error) return core.Fail(core.E(opCommitToJournal, "ensure journal schema", err)) } - fieldsJSON, err := marshalJSONText(fields, opCommitToJournal, "marshal fields") - if err != nil { - return core.Fail(err) + fieldsJSON, result := marshalJSONText(fields, opCommitToJournal, "marshal fields") + if !result.OK { + return result } - tagsJSON, err := marshalJSONText(tags, opCommitToJournal, "marshal tags") - if err != nil { - return core.Fail(err) + tagsJSON, result := marshalJSONText(tags, opCommitToJournal, "marshal tags") + if !result.OK { + return result } committedAt := time.Now().UnixMilli() - if err := commitJournalEntry( + if result := commitJournalEntry( storeInstance.sqliteDatabase, storeInstance.journalBucket(), measurement, fieldsJSON, tagsJSON, committedAt, - ); err != nil { + ); !result.OK { + err, _ := result.Value.(error) return core.Fail(core.E(opCommitToJournal, "insert journal entry", err)) } @@ -108,10 +110,11 @@ func (storeInstance *Store) CommitToJournal(measurement string, fields map[strin // Usage example: `result := storeInstance.QueryJournal(\`from(bucket: "events") |> range(start: -24h) |> filter(fn: (r) => r.workspace == "session-a")\`)` // Usage example: `result := storeInstance.QueryJournal("SELECT measurement, committed_at FROM journal_entries ORDER BY committed_at")` func (storeInstance *Store) QueryJournal(flux string) core.Result { - if err := storeInstance.ensureReady(opQueryJournal); err != nil { - return core.Fail(err) + if result := storeInstance.ensureReady(opQueryJournal); !result.OK { + return result } - if err := ensureJournalSchema(storeInstance.sqliteDatabase); err != nil { + if result := ensureJournalSchema(storeInstance.sqliteDatabase); !result.OK { + err, _ := result.Value.(error) return core.Fail(core.E(opQueryJournal, "ensure journal schema", err)) } @@ -125,9 +128,9 @@ func (storeInstance *Store) QueryJournal(flux string) core.Result { return storeInstance.queryJournalRows(trimmedQuery) } - selectSQL, arguments, err := storeInstance.queryJournalFromFlux(trimmedQuery) - if err != nil { - return core.Fail(err) + selectSQL, arguments, result := storeInstance.queryJournalFromFlux(trimmedQuery) + if !result.OK { + return result } return storeInstance.queryJournalRows(selectSQL, arguments...) } @@ -147,22 +150,23 @@ func (storeInstance *Store) queryJournalRows(query string, arguments ...any) cor } defer func() { _ = rows.Close() }() - rowMaps, err := queryRowsAsMaps(rows) - if err != nil { + rowMaps, result := queryRowsAsMaps(rows) + if !result.OK { + err, _ := result.Value.(error) return core.Fail(core.E(opQueryJournal, "scan rows", err)) } return core.Ok(inflateJournalRows(rowMaps)) } -func (storeInstance *Store) queryJournalFromFlux(flux string) (string, []any, error) { +func (storeInstance *Store) queryJournalFromFlux(flux string) (string, []any, core.Result) { queryBuilder := core.NewBuilder() queryBuilder.WriteString("SELECT bucket_name, measurement, fields_json, tags_json, committed_at, archived_at FROM ") queryBuilder.WriteString(journalEntriesTableName) queryBuilder.WriteString(" WHERE archived_at IS NULL") - filters, err := journalFluxSQLFilters(flux) - if err != nil { - return "", nil, err + filters, result := journalFluxSQLFilters(flux) + if !result.OK { + return "", nil, result } var queryArguments []any for _, filter := range filters { @@ -171,7 +175,7 @@ func (storeInstance *Store) queryJournalFromFlux(flux string) (string, []any, er } queryBuilder.WriteString(" ORDER BY committed_at, entry_id") - return queryBuilder.String(), queryArguments, nil + return queryBuilder.String(), queryArguments, core.Ok(nil) } type journalSQLFilter struct { @@ -179,17 +183,17 @@ type journalSQLFilter struct { args []any } -func journalFluxSQLFilters(flux string) ([]journalSQLFilter, error) { +func journalFluxSQLFilters(flux string) ([]journalSQLFilter, core.Result) { var filters []journalSQLFilter filters = append(filters, journalNamedFluxFilters(flux)...) - rangeFilters, err := journalRangeSQLFilters(flux) - if err != nil { - return nil, err + rangeFilters, result := journalRangeSQLFilters(flux) + if !result.OK { + return nil, result } filters = append(filters, rangeFilters...) filters = append(filters, journalBucketEqualitySQLFilters(flux)...) filters = append(filters, journalEqualitySQLFilters(flux)...) - return filters, nil + return filters, core.Ok(nil) } func journalNamedFluxFilters(flux string) []journalSQLFilter { @@ -203,24 +207,26 @@ func journalNamedFluxFilters(flux string) []journalSQLFilter { return filters } -func journalRangeSQLFilters(flux string) ([]journalSQLFilter, error) { +func journalRangeSQLFilters(flux string) ([]journalSQLFilter, core.Result) { startRange, stopRange := journalRangeBounds(flux) var filters []journalSQLFilter if startRange != "" { - startTime, err := parseFluxTime(core.Trim(startRange)) - if err != nil { - return nil, core.E(opQueryJournal, "parse range", err) + startTime, result := parseFluxTime(core.Trim(startRange)) + if !result.OK { + err, _ := result.Value.(error) + return nil, core.Fail(core.E(opQueryJournal, "parse range", err)) } filters = append(filters, journalSQLFilter{clause: " AND committed_at >= ?", args: []any{startTime.UnixMilli()}}) } if stopRange != "" { - stopTime, err := parseFluxTime(core.Trim(stopRange)) - if err != nil { - return nil, core.E(opQueryJournal, "parse range", err) + stopTime, result := parseFluxTime(core.Trim(stopRange)) + if !result.OK { + err, _ := result.Value.(error) + return nil, core.Fail(core.E(opQueryJournal, "parse range", err)) } filters = append(filters, journalSQLFilter{clause: " AND committed_at < ?", args: []any{stopTime.UnixMilli()}}) } - return filters, nil + return filters, core.Ok(nil) } func journalBucketEqualitySQLFilters(flux string) []journalSQLFilter { @@ -263,23 +269,23 @@ func (storeInstance *Store) journalBucket() string { return storeInstance.journalConfiguration.BucketName } -func ensureJournalSchema(database schemaDatabase) error { +func ensureJournalSchema(database schemaDatabase) core.Result { if _, err := database.Exec(createJournalEntriesTableSQL); err != nil { - return err + return core.Fail(err) } if _, err := database.Exec( "CREATE INDEX IF NOT EXISTS journal_entries_bucket_committed_at_idx ON " + journalEntriesTableName + " (bucket_name, committed_at)", ); err != nil { - return err + return core.Fail(err) } - return nil + return core.Ok(nil) } func commitJournalEntry( executor journalExecutor, bucket, measurement, fieldsJSON, tagsJSON string, committedAt int64, -) error { +) core.Result { _, err := executor.Exec( sqlInsertIntoPrefix+journalEntriesTableName+" (bucket_name, measurement, fields_json, tags_json, committed_at, archived_at) VALUES (?, ?, ?, ?, ?, NULL)", bucket, @@ -288,15 +294,18 @@ func commitJournalEntry( tagsJSON, committedAt, ) - return err + if err != nil { + return core.Fail(err) + } + return core.Ok(nil) } -func marshalJSONText(value any, operation, message string) (string, error) { +func marshalJSONText(value any, operation, message string) (string, core.Result) { result := core.JSONMarshal(value) if !result.OK { - return "", core.E(operation, message, result.Value.(error)) + return "", core.Fail(core.E(operation, message, result.Value.(error))) } - return string(result.Value.([]byte)), nil + return string(result.Value.([]byte)), core.Ok(nil) } func journalRangeBounds(flux string) (string, string) { @@ -358,10 +367,10 @@ func indexOfSubstring(text, substring string) int { return -1 } -func parseFluxTime(value string) (time.Time, error) { +func parseFluxTime(value string) (time.Time, core.Result) { value = core.Trim(value) if value == "" { - return time.Time{}, core.E("store.parseFluxTime", "range value is empty", nil) + return time.Time{}, core.Fail(core.E("store.parseFluxTime", "range value is empty", nil)) } value = firstStringOrEmpty(core.Split(value, ",")) value = core.Trim(value) @@ -372,24 +381,24 @@ func parseFluxTime(value string) (time.Time, error) { value = core.TrimSuffix(core.TrimPrefix(value, `"`), `"`) } if value == "now()" { - return time.Now(), nil + return time.Now(), core.Ok(nil) } if core.HasSuffix(value, "d") { - days, err := parseJournalInt64(core.TrimSuffix(value, "d")) - if err != nil { - return time.Time{}, err + days, result := parseJournalInt64(core.TrimSuffix(value, "d")) + if !result.OK { + return time.Time{}, result } - return time.Now().Add(time.Duration(days) * 24 * time.Hour), nil + return time.Now().Add(time.Duration(days) * 24 * time.Hour), core.Ok(nil) } lookback, err := time.ParseDuration(value) if err == nil { - return time.Now().Add(lookback), nil + return time.Now().Add(lookback), core.Ok(nil) } parsedTime, err := time.Parse(time.RFC3339Nano, value) if err != nil { - return time.Time{}, err + return time.Time{}, core.Fail(err) } - return parsedTime, nil + return parsedTime, core.Ok(nil) } func quotedSubmatch(pattern *regexp.Regexp, value string) string { @@ -409,10 +418,10 @@ func firstQuotedSubmatch(patterns []*regexp.Regexp, value string) string { return "" } -func queryRowsAsMaps(rows *sql.Rows) ([]map[string]any, error) { +func queryRowsAsMaps(rows *sql.Rows) ([]map[string]any, core.Result) { columnNames, err := rows.Columns() if err != nil { - return nil, err + return nil, core.Fail(err) } var result []map[string]any @@ -423,7 +432,7 @@ func queryRowsAsMaps(rows *sql.Rows) ([]map[string]any, error) { scanTargets[i] = &rawValues[i] } if err := rows.Scan(scanTargets...); err != nil { - return nil, err + return nil, core.Fail(err) } row := make(map[string]any, len(columnNames)) @@ -433,9 +442,9 @@ func queryRowsAsMaps(rows *sql.Rows) ([]map[string]any, error) { result = append(result, row) } if err := rows.Err(); err != nil { - return nil, err + return nil, core.Fail(err) } - return result, nil + return result, core.Ok(nil) } func inflateJournalRows(rows []map[string]any) []map[string]any { @@ -530,54 +539,54 @@ func parseJournalScalarValue(value string) (any, bool) { return false, true } - if integerValue, err := parseJournalInt64(value); err == nil { + if integerValue, result := parseJournalInt64(value); result.OK { return integerValue, true } - if floatValue, err := parseJournalFloat64(value); err == nil { + if floatValue, result := parseJournalFloat64(value); result.OK { return floatValue, true } return nil, false } -func parseJournalInt64(value string) (int64, error) { - prefix, err := parseJournalNumberPrefix(value, opParseJournalInt64, "integer") - if err != nil { - return 0, err +func parseJournalInt64(value string) (int64, core.Result) { + prefix, result := parseJournalNumberPrefix(value, opParseJournalInt64, "integer") + if !result.OK { + return 0, result } limit := journalIntegerLimit(prefix.negative) - parsed, err := parseJournalUnsignedInteger(value, prefix.index, limit) - if err != nil { - return 0, err + parsed, result := parseJournalUnsignedInteger(value, prefix.index, limit) + if !result.OK { + return 0, result } if prefix.negative { if parsed == uint64(1<<63) { - return -1 << 63, nil + return -1 << 63, core.Ok(nil) } - return -int64(parsed), nil + return -int64(parsed), core.Ok(nil) } - return int64(parsed), nil + return int64(parsed), core.Ok(nil) } -func parseJournalFloat64(value string) (float64, error) { - prefix, err := parseJournalNumberPrefix(value, opParseJournalFloat64, "float") - if err != nil { - return 0, err +func parseJournalFloat64(value string) (float64, core.Result) { + prefix, result := parseJournalNumberPrefix(value, opParseJournalFloat64, "float") + if !result.OK { + return 0, result } - parsed, index, digits, err := parseJournalFloatWhole(value, prefix.index) - if err != nil { - return 0, err + parsed, index, digits, result := parseJournalFloatWhole(value, prefix.index) + if !result.OK { + return 0, result } parsed, index, digits = parseJournalFloatFraction(value, index, parsed, digits) if digits == 0 { - return 0, core.E(opParseJournalFloat64, "float value has no digits", nil) + return 0, core.Fail(core.E(opParseJournalFloat64, "float value has no digits", nil)) } if index != len(value) { - return 0, core.E(opParseJournalFloat64, "float value contains invalid characters", nil) + return 0, core.Fail(core.E(opParseJournalFloat64, "float value contains invalid characters", nil)) } if prefix.negative { - return -parsed, nil + return -parsed, core.Ok(nil) } - return parsed, nil + return parsed, core.Ok(nil) } const maxJournalFloat64 = 1.79769313486231570814527423731704357e+308 @@ -587,19 +596,19 @@ type journalNumberPrefix struct { index int } -func parseJournalNumberPrefix(value, operation, valueName string) (journalNumberPrefix, error) { +func parseJournalNumberPrefix(value, operation, valueName string) (journalNumberPrefix, core.Result) { if value == "" { - return journalNumberPrefix{}, core.E(operation, core.Concat(valueName, " value is empty"), nil) + return journalNumberPrefix{}, core.Fail(core.E(operation, core.Concat(valueName, " value is empty"), nil)) } prefix := journalNumberPrefix{} if value[0] == '-' || value[0] == '+' { prefix.negative = value[0] == '-' prefix.index = 1 if prefix.index == len(value) { - return journalNumberPrefix{}, core.E(operation, core.Concat(valueName, " value has no digits"), nil) + return journalNumberPrefix{}, core.Fail(core.E(operation, core.Concat(valueName, " value has no digits"), nil)) } } - return prefix, nil + return prefix, core.Ok(nil) } func journalIntegerLimit(negative bool) uint64 { @@ -609,34 +618,34 @@ func journalIntegerLimit(negative bool) uint64 { return uint64(1<<63 - 1) } -func parseJournalUnsignedInteger(value string, index int, limit uint64) (uint64, error) { +func parseJournalUnsignedInteger(value string, index int, limit uint64) (uint64, core.Result) { var parsed uint64 for ; index < len(value); index++ { character := value[index] if character < '0' || character > '9' { - return 0, core.E(opParseJournalInt64, "integer value contains non-digit characters", nil) + return 0, core.Fail(core.E(opParseJournalInt64, "integer value contains non-digit characters", nil)) } digit := uint64(character - '0') if parsed > (limit-digit)/10 { - return 0, core.E(opParseJournalInt64, "integer value is out of range", nil) + return 0, core.Fail(core.E(opParseJournalInt64, "integer value is out of range", nil)) } parsed = parsed*10 + digit } - return parsed, nil + return parsed, core.Ok(nil) } -func parseJournalFloatWhole(value string, index int) (float64, int, int, error) { +func parseJournalFloatWhole(value string, index int) (float64, int, int, core.Result) { var parsed float64 digits := 0 for index < len(value) && isJournalDigit(value[index]) { parsed = parsed*10 + float64(value[index]-'0') if parsed > maxJournalFloat64 { - return 0, index, digits, core.E(opParseJournalFloat64, "float value is out of range", nil) + return 0, index, digits, core.Fail(core.E(opParseJournalFloat64, "float value is out of range", nil)) } digits++ index++ } - return parsed, index, digits, nil + return parsed, index, digits, core.Ok(nil) } func parseJournalFloatFraction(value string, index int, parsed float64, digits int) (float64, int, int) { diff --git a/go/journal_example_test.go b/go/journal_example_test.go new file mode 100644 index 0000000..21154e0 --- /dev/null +++ b/go/journal_example_test.go @@ -0,0 +1,19 @@ +package store + +import core "dappco.re/go" + +func ExampleStore_CommitToJournal() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.CommitToJournal("measurement", map[string]any{"value": 1}, map[string]string{"kind": "example"}) + exampleRequireOK(result) +} + +func ExampleStore_QueryJournal() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + exampleRequireOK(storeInstance.CommitToJournal("measurement", map[string]any{"value": 1}, map[string]string{"kind": "example"})) + result := storeInstance.QueryJournal("SELECT measurement FROM journal_entries") + exampleRequireOK(result) + core.Println(result.Value) +} diff --git a/journal_test.go b/go/journal_test.go similarity index 96% rename from journal_test.go rename to go/journal_test.go index bd593be..bba74cd 100644 --- a/journal_test.go +++ b/go/journal_test.go @@ -224,18 +224,18 @@ func TestJournal_QueryJournal_Good_AbsoluteRangeWithStop(t *testing.T) { assertTrue(t, storeInstance.CommitToJournal(testSessionA, map[string]any{"like": 1}, map[string]string{"workspace": testSessionA}).OK) assertTrue(t, storeInstance.CommitToJournal(testSessionB, map[string]any{"like": 2}, map[string]string{"workspace": testSessionB}).OK) - _, err = storeInstance.sqliteDatabase.Exec( + _, sqlErr := storeInstance.sqliteDatabase.Exec( testSQLUpdatePrefix+journalEntriesTableName+testSetCommittedAtByMeasurementSQL, time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC).UnixMilli(), testSessionA, ) - assertNoError(t, err) - _, err = storeInstance.sqliteDatabase.Exec( + assertNoError(t, sqlErr) + _, sqlErr = storeInstance.sqliteDatabase.Exec( testSQLUpdatePrefix+journalEntriesTableName+testSetCommittedAtByMeasurementSQL, time.Date(2026, 3, 30, 12, 0, 0, 0, time.UTC).UnixMilli(), testSessionB, ) - assertNoError(t, err) + assertNoError(t, sqlErr) rows := requireResultRows( t, @@ -253,18 +253,18 @@ func TestJournal_QueryJournal_Good_AbsoluteRangeHonoursStop(t *testing.T) { assertTrue(t, storeInstance.CommitToJournal(testSessionA, map[string]any{"like": 1}, map[string]string{"workspace": testSessionA}).OK) assertTrue(t, storeInstance.CommitToJournal(testSessionB, map[string]any{"like": 2}, map[string]string{"workspace": testSessionB}).OK) - _, err = storeInstance.sqliteDatabase.Exec( + _, sqlErr := storeInstance.sqliteDatabase.Exec( testSQLUpdatePrefix+journalEntriesTableName+testSetCommittedAtByMeasurementSQL, time.Date(2026, 3, 29, 12, 0, 0, 0, time.UTC).UnixMilli(), testSessionA, ) - assertNoError(t, err) - _, err = storeInstance.sqliteDatabase.Exec( + assertNoError(t, sqlErr) + _, sqlErr = storeInstance.sqliteDatabase.Exec( testSQLUpdatePrefix+journalEntriesTableName+testSetCommittedAtByMeasurementSQL, time.Date(2026, 3, 30, 12, 0, 0, 0, time.UTC).UnixMilli(), testSessionB, ) - assertNoError(t, err) + assertNoError(t, sqlErr) rows := requireResultRows( t, @@ -285,28 +285,28 @@ func TestJournal_CommitToJournal_Bad_EmptyMeasurement(t *testing.T) { } func TestJournal_Store_CommitToJournal_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) result := storeInstance.CommitToJournal("measurement", map[string]any{"value": 1}, map[string]string{"kind": "ax7"}) AssertTrue(t, result.OK) AssertEqual(t, "measurement", result.Value.(map[string]any)["measurement"]) } func TestJournal_Store_CommitToJournal_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) result := storeInstance.CommitToJournal("", nil, nil) AssertFalse(t, result.OK) AssertContains(t, result.Error(), "measurement") } func TestJournal_Store_CommitToJournal_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) result := storeInstance.CommitToJournal("measurement", nil, nil) AssertTrue(t, result.OK) AssertNotNil(t, result.Value) } func TestJournal_Store_QueryJournal_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireTrue(t, storeInstance.CommitToJournal("measurement", map[string]any{"value": 1}, nil).OK) result := storeInstance.QueryJournal("SELECT measurement FROM journal_entries") AssertTrue(t, result.OK) @@ -314,14 +314,14 @@ func TestJournal_Store_QueryJournal_Good(t *T) { } func TestJournal_Store_QueryJournal_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) result := storeInstance.QueryJournal("from(bucket: \"events\") |> range(start: nope)") AssertFalse(t, result.OK) AssertContains(t, result.Error(), "parse") } func TestJournal_Store_QueryJournal_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) result := storeInstance.QueryJournal("") AssertTrue(t, result.OK) AssertEmpty(t, result.Value) diff --git a/json.go b/go/json.go similarity index 59% rename from json.go rename to go/json.go index 6fb9490..04d2f0a 100644 --- a/json.go +++ b/go/json.go @@ -19,25 +19,25 @@ import core "dappco.re/go" // cacheEntry := CacheEntry{Data: store.RawMessage([]byte("{\"name\":\"Alice\"}"))} type RawMessage []byte -// MarshalJSON returns the raw bytes as-is. If empty, returns `null`. +// MarshalJSONCore returns the raw bytes as-is. If empty, returns `null`. // -// Usage example: `bytes, err := store.RawMessage([]byte("{\"name\":\"Alice\"}")).MarshalJSON()` -func (raw RawMessage) MarshalJSON() ([]byte, error) { +// Usage example: `bytes, err := store.RawMessage([]byte("{\"name\":\"Alice\"}")).MarshalJSONCore()` +func (raw RawMessage) MarshalJSONCore() ([]byte, core.Result) { if len(raw) == 0 { - return []byte("null"), nil + return []byte("null"), core.Ok(nil) } - return raw, nil + return raw, core.Ok(nil) } -// UnmarshalJSON stores the raw JSON bytes without decoding them. +// UnmarshalJSONCore stores the raw JSON bytes without decoding them. // -// Usage example: `var raw store.RawMessage; err := raw.UnmarshalJSON([]byte("{\"name\":\"Alice\"}"))` -func (raw *RawMessage) UnmarshalJSON(data []byte) error { +// Usage example: `var raw store.RawMessage; err := raw.UnmarshalJSONCore([]byte("{\"name\":\"Alice\"}"))` +func (raw *RawMessage) UnmarshalJSONCore(data []byte) core.Result { if raw == nil { - return core.E("store.RawMessage.UnmarshalJSON", "nil receiver", nil) + return core.Fail(core.E("store.RawMessage.UnmarshalJSONCore", "nil receiver", nil)) } *raw = append((*raw)[:0], data...) - return nil + return core.Ok(nil) } // MarshalIndent serialises a value to pretty-printed JSON bytes. @@ -45,27 +45,28 @@ func (raw *RawMessage) UnmarshalJSON(data []byte) error { // so consumers get readable output without importing encoding/json. // // Usage example: `data, err := store.MarshalIndent(map[string]string{"name": "Alice"}, "", " ")` -func MarshalIndent(value any, prefix, indent string) ([]byte, error) { +func MarshalIndent(value any, prefix, indent string) ([]byte, core.Result) { marshalled := core.JSONMarshal(value) if !marshalled.OK { if err, ok := marshalled.Value.(error); ok { - return nil, core.E(opMarshalIndent, "marshal", err) + return nil, core.Fail(core.E(opMarshalIndent, "marshal", err)) } - return nil, core.E(opMarshalIndent, "marshal", nil) + return nil, core.Fail(core.E(opMarshalIndent, "marshal", nil)) } raw, ok := marshalled.Value.([]byte) if !ok { - return nil, core.E(opMarshalIndent, "non-bytes result", nil) + return nil, core.Fail(core.E(opMarshalIndent, "non-bytes result", nil)) } if prefix == "" && indent == "" { - return raw, nil + return raw, core.Ok(nil) } buf := core.NewBuilder() - if err := indentCompactJSON(buf, raw, prefix, indent); err != nil { - return nil, core.E(opMarshalIndent, "indent", err) + if result := indentCompactJSON(buf, raw, prefix, indent); !result.OK { + err, _ := result.Value.(error) + return nil, core.Fail(core.E(opMarshalIndent, "indent", err)) } - return []byte(buf.String()), nil + return []byte(buf.String()), core.Ok(nil) } // indentCompactJSON formats compact JSON bytes with prefix+indent. @@ -75,37 +76,37 @@ func MarshalIndent(value any, prefix, indent string) ([]byte, error) { func indentCompactJSON(buf interface { WriteByte(byte) error WriteString(string) (int, error) -}, src []byte, prefix, indent string) error { +}, src []byte, prefix, indent string) core.Result { state := compactJSONIndentState{} - writeNewlineIndent := func(level int) error { + writeNewlineIndent := func(level int) core.Result { if err := buf.WriteByte('\n'); err != nil { - return err + return core.Fail(err) } if _, err := buf.WriteString(prefix); err != nil { - return err + return core.Fail(err) } for i := 0; i < level; i++ { if _, err := buf.WriteString(indent); err != nil { - return err + return core.Fail(err) } } - return nil + return core.Ok(nil) } for i := 0; i < len(src); i++ { c := src[i] if state.inString { - if err := state.writeStringByte(buf, c); err != nil { - return err + if result := state.writeStringByte(buf, c); !result.OK { + return result } continue } - if err := state.writeValueByte(buf, src, i, writeNewlineIndent); err != nil { - return err + if result := state.writeValueByte(buf, src, i, writeNewlineIndent); !result.OK { + return result } } - return nil + return core.Ok(nil) } type compactJSONIndentState struct { @@ -116,76 +117,88 @@ type compactJSONIndentState struct { func (state *compactJSONIndentState) writeStringByte(buf interface { WriteByte(byte) error -}, c byte) error { +}, c byte) core.Result { if err := buf.WriteByte(c); err != nil { - return err + return core.Fail(err) } if state.escaped { state.escaped = false - return nil + return core.Ok(nil) } if c == '\\' { state.escaped = true - return nil + return core.Ok(nil) } if c == '"' { state.inString = false } - return nil + return core.Ok(nil) } func (state *compactJSONIndentState) writeValueByte(buf interface { WriteByte(byte) error -}, src []byte, index int, writeNewlineIndent func(int) error) error { +}, src []byte, index int, writeNewlineIndent func(int) core.Result) core.Result { c := src[index] switch c { case '"': state.inString = true - return buf.WriteByte(c) + if err := buf.WriteByte(c); err != nil { + return core.Fail(err) + } + return core.Ok(nil) case '{', '[': return state.writeOpeningValueByte(buf, src, index, writeNewlineIndent) case '}', ']': return state.writeClosingValueByte(buf, src, index, writeNewlineIndent) case ',': if err := buf.WriteByte(c); err != nil { - return err + return core.Fail(err) } return writeNewlineIndent(state.depth) case ':': if err := buf.WriteByte(c); err != nil { - return err + return core.Fail(err) } - return buf.WriteByte(' ') + if err := buf.WriteByte(' '); err != nil { + return core.Fail(err) + } + return core.Ok(nil) case ' ', '\t', '\n', '\r': - return nil + return core.Ok(nil) default: - return buf.WriteByte(c) + if err := buf.WriteByte(c); err != nil { + return core.Fail(err) + } + return core.Ok(nil) } } func (state *compactJSONIndentState) writeOpeningValueByte(buf interface { WriteByte(byte) error -}, src []byte, index int, writeNewlineIndent func(int) error) error { +}, src []byte, index int, writeNewlineIndent func(int) core.Result) core.Result { if err := buf.WriteByte(src[index]); err != nil { - return err + return core.Fail(err) } state.depth++ if index+1 < len(src) && (src[index+1] == '}' || src[index+1] == ']') { - return nil + return core.Ok(nil) } return writeNewlineIndent(state.depth) } func (state *compactJSONIndentState) writeClosingValueByte(buf interface { WriteByte(byte) error -}, src []byte, index int, writeNewlineIndent func(int) error) error { +}, src []byte, index int, writeNewlineIndent func(int) core.Result) core.Result { if index > 0 && src[index-1] != '{' && src[index-1] != '[' { state.depth-- - if err := writeNewlineIndent(state.depth); err != nil { - return err + if result := writeNewlineIndent(state.depth); !result.OK { + return result } } else { state.depth-- } - return buf.WriteByte(src[index]) + if err := buf.WriteByte(src[index]); err != nil { + return core.Fail(err) + } + return core.Ok(nil) } diff --git a/go/json_example_test.go b/go/json_example_test.go new file mode 100644 index 0000000..5ada5b9 --- /dev/null +++ b/go/json_example_test.go @@ -0,0 +1,23 @@ +package store + +import core "dappco.re/go" + +func ExampleRawMessage_MarshalJSONCore() { + raw := RawMessage([]byte(`{"name":"Ada"}`)) + data, result := raw.MarshalJSONCore() + exampleRequireOK(result) + core.Println(string(data)) +} + +func ExampleRawMessage_UnmarshalJSONCore() { + var raw RawMessage + result := raw.UnmarshalJSONCore([]byte(`{"name":"Ada"}`)) + exampleRequireOK(result) + core.Println(string(raw)) +} + +func ExampleMarshalIndent() { + data, result := MarshalIndent(map[string]string{"name": "Ada"}, "", " ") + exampleRequireOK(result) + core.Println(string(data)) +} diff --git a/json_test.go b/go/json_test.go similarity index 67% rename from json_test.go rename to go/json_test.go index e351064..0ed2398 100644 --- a/json_test.go +++ b/go/json_test.go @@ -1,48 +1,47 @@ package store_test import ( - . "dappco.re/go" store "dappco.re/go/store" ) -func TestJson_RawMessage_MarshalJSON_Good(t *T) { +func TestJson_RawMessage_MarshalJSONCore_Good(t *T) { raw := store.RawMessage([]byte(`{"name":"alice"}`)) - data, err := raw.MarshalJSON() + data, err := raw.MarshalJSONCore() AssertNoError(t, err) AssertEqual(t, `{"name":"alice"}`, string(data)) } -func TestJson_RawMessage_MarshalJSON_Bad(t *T) { +func TestJson_RawMessage_MarshalJSONCore_Bad(t *T) { raw := store.RawMessage(nil) - data, err := raw.MarshalJSON() + data, err := raw.MarshalJSONCore() AssertNoError(t, err) AssertEqual(t, "null", string(data)) } -func TestJson_RawMessage_MarshalJSON_Ugly(t *T) { +func TestJson_RawMessage_MarshalJSONCore_Ugly(t *T) { raw := store.RawMessage([]byte("not-json")) - data, err := raw.MarshalJSON() + data, err := raw.MarshalJSONCore() AssertNoError(t, err) AssertEqual(t, "not-json", string(data)) } -func TestJson_RawMessage_UnmarshalJSON_Good(t *T) { +func TestJson_RawMessage_UnmarshalJSONCore_Good(t *T) { var raw store.RawMessage - err := raw.UnmarshalJSON([]byte(`{"name":"alice"}`)) + err := raw.UnmarshalJSONCore([]byte(`{"name":"alice"}`)) AssertNoError(t, err) AssertEqual(t, `{"name":"alice"}`, string(raw)) } -func TestJson_RawMessage_UnmarshalJSON_Bad(t *T) { +func TestJson_RawMessage_UnmarshalJSONCore_Bad(t *T) { var raw *store.RawMessage - err := raw.UnmarshalJSON([]byte(`{"name":"alice"}`)) + err := raw.UnmarshalJSONCore([]byte(`{"name":"alice"}`)) AssertError(t, err) AssertNil(t, raw) } -func TestJson_RawMessage_UnmarshalJSON_Ugly(t *T) { +func TestJson_RawMessage_UnmarshalJSONCore_Ugly(t *T) { raw := store.RawMessage([]byte("old")) - err := raw.UnmarshalJSON([]byte("null")) + err := raw.UnmarshalJSONCore([]byte("null")) AssertNoError(t, err) AssertEqual(t, "null", string(raw)) } diff --git a/medium.go b/go/medium.go similarity index 65% rename from medium.go rename to go/medium.go index d5489d3..daa7d93 100644 --- a/medium.go +++ b/go/medium.go @@ -3,7 +3,6 @@ package store import ( - "bytes" "encoding/csv" goio "io" "io/fs" @@ -20,21 +19,21 @@ import ( // // Usage example: `medium, _ := local.New("/tmp/exports"); storeInstance, err := store.NewConfigured(store.StoreConfig{DatabasePath: ":memory:", Medium: medium})` type Medium interface { - Read(path string) (string, error) - Write(path, content string) error - WriteMode(path, content string, mode fs.FileMode) error - EnsureDir(path string) error + Read(path string) (string, core.Result) + Write(path, content string) core.Result + WriteMode(path, content string, mode fs.FileMode) core.Result + EnsureDir(path string) core.Result IsFile(path string) bool - Delete(path string) error - DeleteAll(path string) error - Rename(oldPath, newPath string) error - List(path string) ([]fs.DirEntry, error) - Stat(path string) (fs.FileInfo, error) - Open(path string) (fs.File, error) - Create(path string) (goio.WriteCloser, error) - Append(path string) (goio.WriteCloser, error) - ReadStream(path string) (goio.ReadCloser, error) - WriteStream(path string) (goio.WriteCloser, error) + Delete(path string) core.Result + DeleteAll(path string) core.Result + Rename(oldPath, newPath string) core.Result + List(path string) ([]fs.DirEntry, core.Result) + Stat(path string) (fs.FileInfo, core.Result) + Open(path string) (fs.File, core.Result) + Create(path string) (goio.WriteCloser, core.Result) + Append(path string) (goio.WriteCloser, core.Result) + ReadStream(path string) (goio.ReadCloser, core.Result) + WriteStream(path string) (goio.WriteCloser, core.Result) Exists(path string) bool IsDir(path string) bool } @@ -48,39 +47,39 @@ func localMedium() Medium { } // Usage example: `content, err := localMedium().Read("archive.jsonl")` -func (medium *filesystemmedium) Read(path string) (string, error) { +func (medium *filesystemmedium) Read(path string) (string, core.Result) { result := medium.filesystem.Read(path) if !result.OK { return "", resultError(result) } - return result.Value.(string), nil + return result.Value.(string), core.Ok(nil) } // Usage example: `err := localMedium().Write("archive.jsonl", payload)` -func (medium *filesystemmedium) Write(path, content string) error { +func (medium *filesystemmedium) Write(path, content string) core.Result { result := medium.filesystem.Write(path, content) if !result.OK { return resultError(result) } - return nil + return core.Ok(nil) } // Usage example: `err := localMedium().WriteMode("archive.jsonl", payload, 0o600)` -func (medium *filesystemmedium) WriteMode(path, content string, mode fs.FileMode) error { +func (medium *filesystemmedium) WriteMode(path, content string, mode fs.FileMode) core.Result { result := medium.filesystem.WriteMode(path, content, mode) if !result.OK { return resultError(result) } - return nil + return core.Ok(nil) } // Usage example: `err := localMedium().EnsureDir("archives")` -func (medium *filesystemmedium) EnsureDir(path string) error { +func (medium *filesystemmedium) EnsureDir(path string) core.Result { result := medium.filesystem.EnsureDir(path) if !result.OK { return resultError(result) } - return nil + return core.Ok(nil) } // Usage example: `exists := localMedium().IsFile("archive.jsonl")` @@ -89,93 +88,93 @@ func (medium *filesystemmedium) IsFile(path string) bool { } // Usage example: `err := localMedium().Delete("archive.jsonl")` -func (medium *filesystemmedium) Delete(path string) error { +func (medium *filesystemmedium) Delete(path string) core.Result { result := medium.filesystem.Delete(path) if !result.OK { return resultError(result) } - return nil + return core.Ok(nil) } // Usage example: `err := localMedium().DeleteAll("archives")` -func (medium *filesystemmedium) DeleteAll(path string) error { +func (medium *filesystemmedium) DeleteAll(path string) core.Result { result := medium.filesystem.DeleteAll(path) if !result.OK { return resultError(result) } - return nil + return core.Ok(nil) } // Usage example: `err := localMedium().Rename("old.jsonl", "new.jsonl")` -func (medium *filesystemmedium) Rename(oldPath, newPath string) error { +func (medium *filesystemmedium) Rename(oldPath, newPath string) core.Result { result := medium.filesystem.Rename(oldPath, newPath) if !result.OK { return resultError(result) } - return nil + return core.Ok(nil) } // Usage example: `entries, err := localMedium().List("archives")` -func (medium *filesystemmedium) List(path string) ([]fs.DirEntry, error) { +func (medium *filesystemmedium) List(path string) ([]fs.DirEntry, core.Result) { result := medium.filesystem.List(path) if !result.OK { return nil, resultError(result) } - return result.Value.([]fs.DirEntry), nil + return result.Value.([]fs.DirEntry), core.Ok(nil) } // Usage example: `info, err := localMedium().Stat("archive.jsonl")` -func (medium *filesystemmedium) Stat(path string) (fs.FileInfo, error) { +func (medium *filesystemmedium) Stat(path string) (fs.FileInfo, core.Result) { result := medium.filesystem.Stat(path) if !result.OK { return nil, resultError(result) } - return result.Value.(fs.FileInfo), nil + return result.Value.(fs.FileInfo), core.Ok(nil) } // Usage example: `file, err := localMedium().Open("archive.jsonl")` -func (medium *filesystemmedium) Open(path string) (fs.File, error) { +func (medium *filesystemmedium) Open(path string) (fs.File, core.Result) { result := medium.filesystem.Open(path) if !result.OK { return nil, resultError(result) } - return result.Value.(fs.File), nil + return result.Value.(fs.File), core.Ok(nil) } // Usage example: `writer, err := localMedium().Create("archive.jsonl")` -func (medium *filesystemmedium) Create(path string) (goio.WriteCloser, error) { +func (medium *filesystemmedium) Create(path string) (goio.WriteCloser, core.Result) { result := medium.filesystem.Create(path) if !result.OK { return nil, resultError(result) } - return result.Value.(goio.WriteCloser), nil + return result.Value.(goio.WriteCloser), core.Ok(nil) } // Usage example: `writer, err := localMedium().Append("archive.jsonl")` -func (medium *filesystemmedium) Append(path string) (goio.WriteCloser, error) { +func (medium *filesystemmedium) Append(path string) (goio.WriteCloser, core.Result) { result := medium.filesystem.Append(path) if !result.OK { return nil, resultError(result) } - return result.Value.(goio.WriteCloser), nil + return result.Value.(goio.WriteCloser), core.Ok(nil) } // Usage example: `reader, err := localMedium().ReadStream("archive.jsonl")` -func (medium *filesystemmedium) ReadStream(path string) (goio.ReadCloser, error) { +func (medium *filesystemmedium) ReadStream(path string) (goio.ReadCloser, core.Result) { result := medium.filesystem.ReadStream(path) if !result.OK { return nil, resultError(result) } - return result.Value.(goio.ReadCloser), nil + return result.Value.(goio.ReadCloser), core.Ok(nil) } // Usage example: `writer, err := localMedium().WriteStream("archive.jsonl")` -func (medium *filesystemmedium) WriteStream(path string) (goio.WriteCloser, error) { +func (medium *filesystemmedium) WriteStream(path string) (goio.WriteCloser, core.Result) { result := medium.filesystem.WriteStream(path) if !result.OK { return nil, resultError(result) } - return result.Value.(goio.WriteCloser), nil + return result.Value.(goio.WriteCloser), core.Ok(nil) } // Usage example: `exists := localMedium().Exists("archive.jsonl")` @@ -188,11 +187,11 @@ func (medium *filesystemmedium) IsDir(path string) bool { return medium.filesystem.IsDir(path) } -func resultError(result core.Result) error { +func resultError(result core.Result) core.Result { if err, ok := result.Value.(error); ok { - return err + return core.Fail(err) } - return core.E("store.Medium", core.Sprint(result.Value), nil) + return core.Fail(core.E("store.Medium", core.Sprint(result.Value), nil)) } // Usage example: `medium, _ := local.New("/srv/core"); storeInstance, err := store.NewConfigured(store.StoreConfig{DatabasePath: ":memory:", Medium: medium})` @@ -222,20 +221,21 @@ func (storeInstance *Store) Medium() Medium { // chosen from the file extension: `.json` expects either a top-level array or // `{"entries":[...]}` shape, `.jsonl`/`.ndjson` parse line-by-line, and `.csv` // uses the first row as the header. -func Import(workspace *Workspace, medium Medium, path string) error { +func Import(workspace *Workspace, medium Medium, path string) core.Result { if workspace == nil { - return core.E(opImport, "workspace is nil", nil) + return core.Fail(core.E(opImport, "workspace is nil", nil)) } if medium == nil { - return core.E(opImport, "medium is nil", nil) + return core.Fail(core.E(opImport, "medium is nil", nil)) } if path == "" { - return core.E(opImport, "path is empty", nil) + return core.Fail(core.E(opImport, "path is empty", nil)) } - content, err := medium.Read(path) - if err != nil { - return core.E(opImport, "read from medium", err) + content, result := medium.Read(path) + if !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImport, "read from medium", err)) } kind := importEntryKind(path) @@ -256,19 +256,20 @@ func Import(workspace *Workspace, medium Medium, path string) error { // path. Format is chosen from the extension: `.jsonl` writes one record per // query row, `.csv` writes header + rows, everything else writes the // aggregate as JSON. -func Export(workspace *Workspace, medium Medium, path string) error { +func Export(workspace *Workspace, medium Medium, path string) core.Result { if workspace == nil { - return core.E(opExport, "workspace is nil", nil) + return core.Fail(core.E(opExport, "workspace is nil", nil)) } if medium == nil { - return core.E(opExport, "medium is nil", nil) + return core.Fail(core.E(opExport, "medium is nil", nil)) } if path == "" { - return core.E(opExport, "path is empty", nil) + return core.Fail(core.E(opExport, "path is empty", nil)) } - if err := ensureMediumDir(medium, core.PathDir(path)); err != nil { - return core.E(opExport, "ensure directory", err) + if result := ensureMediumDir(medium, core.PathDir(path)); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opExport, "ensure directory", err)) } switch lowercaseText(importExtension(path)) { @@ -281,14 +282,15 @@ func Export(workspace *Workspace, medium Medium, path string) error { } } -func ensureMediumDir(medium Medium, directory string) error { +func ensureMediumDir(medium Medium, directory string) core.Result { if directory == "" || directory == "." || directory == "/" { - return nil + return core.Ok(nil) } - if err := medium.EnsureDir(directory); err != nil { - return core.E("store.ensureMediumDir", "ensure directory", err) + if result := medium.EnsureDir(directory); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E("store.ensureMediumDir", "ensure directory", err)) } - return nil + return core.Ok(nil) } func importExtension(path string) string { @@ -315,7 +317,7 @@ func importEntryKind(path string) string { return base } -func importJSONLines(workspace *Workspace, kind, content string) error { +func importJSONLines(workspace *Workspace, kind, content string) core.Result { scanner := core.Split(content, "\n") for _, rawLine := range scanner { line := core.Trim(rawLine) @@ -325,51 +327,54 @@ func importJSONLines(workspace *Workspace, kind, content string) error { record := map[string]any{} if result := core.JSONUnmarshalString(line, &record); !result.OK { err, _ := result.Value.(error) - return core.E(opImport, "parse jsonl line", err) + return core.Fail(core.E(opImport, "parse jsonl line", err)) } - if err := workspace.Put(kind, record); err != nil { - return core.E(opImport, "put jsonl record", err) + if result := workspace.Put(kind, record); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImport, "put jsonl record", err)) } } - return nil + return core.Ok(nil) } -func importJSON(workspace *Workspace, kind, content string) error { +func importJSON(workspace *Workspace, kind, content string) core.Result { trimmed := core.Trim(content) if trimmed == "" { - return nil + return core.Ok(nil) } var topLevel any if result := core.JSONUnmarshalString(trimmed, &topLevel); !result.OK { err, _ := result.Value.(error) - return core.E(opImport, "parse json", err) + return core.Fail(core.E(opImport, "parse json", err)) } - records, err := collectJSONRecords(topLevel) - if err != nil { - return core.E(opImport, "normalise json records", err) + records, result := collectJSONRecords(topLevel) + if !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImport, "normalise json records", err)) } for _, record := range records { - if err := workspace.Put(kind, record); err != nil { - return core.E(opImport, "put json record", err) + if result := workspace.Put(kind, record); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImport, "put json record", err)) } } - return nil + return core.Ok(nil) } -func collectJSONRecords(value any) ([]map[string]any, error) { +func collectJSONRecords(value any) ([]map[string]any, core.Result) { switch shape := value.(type) { case []any: records := make([]map[string]any, 0, len(shape)) for index, entry := range shape { record, ok := entry.(map[string]any) if !ok { - return nil, core.E(opImport, core.Concat("json array element is not an object at index ", core.Sprint(index)), nil) + return nil, core.Fail(core.E(opImport, core.Concat("json array element is not an object at index ", core.Sprint(index)), nil)) } records = append(records, record) } - return records, nil + return records, core.Ok(nil) case map[string]any: if nested, ok := shape["entries"].([]any); ok { return collectJSONRecords(nested) @@ -380,24 +385,24 @@ func collectJSONRecords(value any) ([]map[string]any, error) { if nested, ok := shape["data"].([]any); ok { return collectJSONRecords(nested) } - return []map[string]any{shape}, nil + return []map[string]any{shape}, core.Ok(nil) } - return nil, core.E(opImport, "unsupported json shape", nil) + return nil, core.Fail(core.E(opImport, "unsupported json shape", nil)) } -func importCSV(workspace *Workspace, kind, content string) error { - reader := csv.NewReader(bytes.NewBufferString(content)) +func importCSV(workspace *Workspace, kind, content string) core.Result { + reader := csv.NewReader(core.NewBufferString(content)) reader.FieldsPerRecord = -1 rows, err := reader.ReadAll() if err != nil { - return core.E(opImport, "parse csv", err) + return core.Fail(core.E(opImport, "parse csv", err)) } if len(rows) == 0 { - return nil + return core.Ok(nil) } header := rows[0] if len(header) == 0 { - return nil + return core.Ok(nil) } for _, fields := range rows[1:] { if len(fields) == 0 { @@ -411,30 +416,33 @@ func importCSV(workspace *Workspace, kind, content string) error { record[columnName] = "" } } - if err := workspace.Put(kind, record); err != nil { - return core.E(opImport, "put csv record", err) + if result := workspace.Put(kind, record); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opImport, "put csv record", err)) } } - return nil + return core.Ok(nil) } -func exportJSON(workspace *Workspace, medium Medium, path string) error { - summary, err := workspace.aggregateFields() - if err != nil { - return core.E(opExport, "aggregate workspace", err) +func exportJSON(workspace *Workspace, medium Medium, path string) core.Result { + summary, result := workspace.aggregateFields() + if !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opExport, "aggregate workspace", err)) } content := core.JSONMarshalString(summary) - if err := medium.Write(path, content); err != nil { - return core.E(opExport, "write json", err) + if result := medium.Write(path, content); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opExport, "write json", err)) } - return nil + return core.Ok(nil) } -func exportJSONLines(workspace *Workspace, medium Medium, path string) error { +func exportJSONLines(workspace *Workspace, medium Medium, path string) core.Result { result := workspace.Query("SELECT entry_kind, entry_data, created_at FROM workspace_entries ORDER BY entry_id") if !result.OK { err, _ := result.Value.(error) - return core.E(opExport, "query workspace", err) + return core.Fail(core.E(opExport, "query workspace", err)) } rows, ok := result.Value.([]map[string]any) if !ok { @@ -447,17 +455,18 @@ func exportJSONLines(workspace *Workspace, medium Medium, path string) error { builder.WriteString(line) builder.WriteString("\n") } - if err := medium.Write(path, builder.String()); err != nil { - return core.E(opExport, "write jsonl", err) + if result := medium.Write(path, builder.String()); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opExport, "write jsonl", err)) } - return nil + return core.Ok(nil) } -func exportCSV(workspace *Workspace, medium Medium, path string) error { +func exportCSV(workspace *Workspace, medium Medium, path string) core.Result { result := workspace.Query("SELECT entry_kind, entry_data, created_at FROM workspace_entries ORDER BY entry_id") if !result.OK { err, _ := result.Value.(error) - return core.E(opExport, "query workspace", err) + return core.Fail(core.E(opExport, "query workspace", err)) } rows, ok := result.Value.([]map[string]any) if !ok { @@ -474,10 +483,11 @@ func exportCSV(workspace *Workspace, medium Medium, path string) error { builder.WriteString(csvField(core.Sprint(row["created_at"]))) builder.WriteString("\n") } - if err := medium.Write(path, builder.String()); err != nil { - return core.E(opExport, "write csv", err) + if result := medium.Write(path, builder.String()); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opExport, "write csv", err)) } - return nil + return core.Ok(nil) } func csvField(value string) string { diff --git a/go/medium_example_test.go b/go/medium_example_test.go new file mode 100644 index 0000000..b5356a2 --- /dev/null +++ b/go/medium_example_test.go @@ -0,0 +1,40 @@ +package store + +import core "dappco.re/go" + +func ExampleWithMedium() { + medium := newFixtureMedium() + storeInstance, result := New(":memory:", WithMedium(medium)) + exampleRequireOK(result) + defer exampleCloseStore(storeInstance) + core.Println(storeInstance.Medium() != nil) +} + +func ExampleStore_Medium() { + storeInstance, result := New(":memory:", WithMedium(newFixtureMedium())) + exampleRequireOK(result) + defer exampleCloseStore(storeInstance) + medium := storeInstance.Medium() + core.Println(medium != nil) +} + +func ExampleImport() { + _, workspace := exampleWorkspace() + defer workspace.Discard() + medium := newFixtureMedium() + exampleRequireOK(medium.Write("entries.jsonl", `{"kind":"note","value":"blue"}`)) + result := Import(workspace, medium, "entries.jsonl") + exampleRequireOK(result) +} + +func ExampleExport() { + _, workspace := exampleWorkspace() + defer workspace.Discard() + exampleRequireOK(workspace.Put("note", map[string]any{"value": "blue"})) + medium := newFixtureMedium() + result := Export(workspace, medium, "summary.json") + exampleRequireOK(result) + content, readResult := medium.Read("summary.json") + exampleRequireOK(readResult) + core.Println(content) +} diff --git a/medium_test.go b/go/medium_test.go similarity index 85% rename from medium_test.go rename to go/medium_test.go index d03c4ae..d310a02 100644 --- a/medium_test.go +++ b/go/medium_test.go @@ -3,7 +3,6 @@ package store import ( - "bytes" goio "io" "io/fs" "sync" @@ -24,46 +23,48 @@ func newMemoryMedium() *memoryMedium { return &memoryMedium{files: make(map[string]string)} } -func (medium *memoryMedium) Read(path string) (string, error) { +func (medium *memoryMedium) Read(path string) (string, core.Result) { medium.lock.Lock() defer medium.lock.Unlock() content, ok := medium.files[path] if !ok { - return "", core.E("memoryMedium.Read", testFileNotFoundPrefix+path, nil) + return "", core.Fail(core.E("memoryMedium.Read", testFileNotFoundPrefix+path, nil)) } - return content, nil + return content, core.Ok(nil) } -func (medium *memoryMedium) Write(path, content string) error { +func (medium *memoryMedium) Write(path, content string) core.Result { medium.lock.Lock() defer medium.lock.Unlock() medium.files[path] = content - return nil + return core.Ok(nil) } -func (medium *memoryMedium) WriteMode(path, content string, _ fs.FileMode) error { +func (medium *memoryMedium) WriteMode(path, content string, _ fs.FileMode) core.Result { return medium.Write(path, content) } -func (medium *memoryMedium) EnsureDir(string) error { return nil } +func (medium *memoryMedium) EnsureDir(string) core.Result { return core.Ok(nil) } -func (medium *memoryMedium) Create(path string) (goio.WriteCloser, error) { - return &memoryWriter{medium: medium, path: path}, nil +func (medium *memoryMedium) Create(path string) (goio.WriteCloser, core.Result) { + return &memoryWriter{medium: medium, path: path, buffer: core.NewBuilder()}, core.Ok(nil) } -func (medium *memoryMedium) Append(path string) (goio.WriteCloser, error) { +func (medium *memoryMedium) Append(path string) (goio.WriteCloser, core.Result) { medium.lock.Lock() defer medium.lock.Unlock() - return &memoryWriter{medium: medium, path: path, buffer: *bytes.NewBufferString(medium.files[path])}, nil + buffer := core.NewBuilder() + buffer.WriteString(medium.files[path]) + return &memoryWriter{medium: medium, path: path, buffer: buffer}, core.Ok(nil) } -func (medium *memoryMedium) ReadStream(path string) (goio.ReadCloser, error) { +func (medium *memoryMedium) ReadStream(path string) (goio.ReadCloser, core.Result) { medium.lock.Lock() defer medium.lock.Unlock() - return goio.NopCloser(bytes.NewReader([]byte(medium.files[path]))), nil + return goio.NopCloser(core.NewReader(medium.files[path])), core.Ok(nil) } -func (medium *memoryMedium) WriteStream(path string) (goio.WriteCloser, error) { +func (medium *memoryMedium) WriteStream(path string) (goio.WriteCloser, core.Result) { return medium.Create(path) } @@ -76,14 +77,14 @@ func (medium *memoryMedium) Exists(path string) bool { func (medium *memoryMedium) IsFile(path string) bool { return medium.Exists(path) } -func (medium *memoryMedium) Delete(path string) error { +func (medium *memoryMedium) Delete(path string) core.Result { medium.lock.Lock() defer medium.lock.Unlock() delete(medium.files, path) - return nil + return core.Ok(nil) } -func (medium *memoryMedium) DeleteAll(path string) error { +func (medium *memoryMedium) DeleteAll(path string) core.Result { medium.lock.Lock() defer medium.lock.Unlock() for key := range medium.files { @@ -91,27 +92,27 @@ func (medium *memoryMedium) DeleteAll(path string) error { delete(medium.files, key) } } - return nil + return core.Ok(nil) } -func (medium *memoryMedium) Rename(oldPath, newPath string) error { +func (medium *memoryMedium) Rename(oldPath, newPath string) core.Result { medium.lock.Lock() defer medium.lock.Unlock() content, ok := medium.files[oldPath] if !ok { - return core.E("memoryMedium.Rename", testFileNotFoundPrefix+oldPath, nil) + return core.Fail(core.E("memoryMedium.Rename", testFileNotFoundPrefix+oldPath, nil)) } medium.files[newPath] = content delete(medium.files, oldPath) - return nil + return core.Ok(nil) } type renameFailMedium struct { *memoryMedium } -func (medium *renameFailMedium) Rename(string, string) error { - return core.E("renameFailMedium.Rename", "forced rename failure", nil) +func (medium *renameFailMedium) Rename(string, string) core.Result { + return core.Fail(core.E("renameFailMedium.Rename", "forced rename failure", nil)) } type writeFailOnceMedium struct { @@ -119,28 +120,28 @@ type writeFailOnceMedium struct { failures int } -func (medium *writeFailOnceMedium) Write(path, content string) error { +func (medium *writeFailOnceMedium) Write(path, content string) core.Result { if medium.failures > 0 { medium.failures-- - return core.E("writeFailOnceMedium.Write", "forced write failure", nil) + return core.Fail(core.E("writeFailOnceMedium.Write", "forced write failure", nil)) } return medium.memoryMedium.Write(path, content) } -func (medium *memoryMedium) List(path string) ([]fs.DirEntry, error) { return nil, nil } +func (medium *memoryMedium) List(path string) ([]fs.DirEntry, core.Result) { return nil, core.Ok(nil) } -func (medium *memoryMedium) Stat(path string) (fs.FileInfo, error) { +func (medium *memoryMedium) Stat(path string) (fs.FileInfo, core.Result) { if !medium.Exists(path) { - return nil, core.E("memoryMedium.Stat", testFileNotFoundPrefix+path, nil) + return nil, core.Fail(core.E("memoryMedium.Stat", testFileNotFoundPrefix+path, nil)) } - return fileInfoStub{name: core.PathBase(path)}, nil + return fileInfoStub{name: core.PathBase(path)}, core.Ok(nil) } -func (medium *memoryMedium) Open(path string) (fs.File, error) { +func (medium *memoryMedium) Open(path string) (fs.File, core.Result) { if !medium.Exists(path) { - return nil, core.E("memoryMedium.Open", testFileNotFoundPrefix+path, nil) + return nil, core.Fail(core.E("memoryMedium.Open", testFileNotFoundPrefix+path, nil)) } - return newMemoryFile(path, medium.files[path]), nil + return newMemoryFile(path, medium.files[path]), core.Ok(nil) } func (medium *memoryMedium) IsDir(string) bool { return false } @@ -148,7 +149,11 @@ func (medium *memoryMedium) IsDir(string) bool { return false } type memoryWriter struct { medium *memoryMedium path string - buffer bytes.Buffer + buffer interface { + Write([]byte) (int, error) + WriteString(string) (int, error) + String() string + } closed bool } @@ -161,7 +166,12 @@ func (writer *memoryWriter) Close() error { return nil } writer.closed = true - return writer.medium.Write(writer.path, writer.buffer.String()) + result := writer.medium.Write(writer.path, writer.buffer.String()) + if result.OK { + return nil + } + err, _ := result.Value.(error) + return err } type fileInfoStub struct { @@ -176,12 +186,16 @@ func (fileInfoStub) Sys() any { return nil } func (info fileInfoStub) Name() string { return info.name } type memoryFile struct { - *bytes.Reader - name string + reader goio.Reader + name string } func newMemoryFile(name, content string) *memoryFile { - return &memoryFile{Reader: bytes.NewReader([]byte(content)), name: name} + return &memoryFile{reader: core.NewReader(content), name: name} +} + +func (file *memoryFile) Read(p []byte) (int, error) { + return file.reader.Read(p) } func (file *memoryFile) Stat() (fs.FileInfo, error) { @@ -577,13 +591,13 @@ func TestMedium_WithMedium_Bad(t *T) { } func TestMedium_WithMedium_Ugly(t *T) { - option := WithMedium(newAX7Medium()) + option := WithMedium(newFixtureMedium()) AssertNotPanics(t, func() { option(nil) }) AssertNotNil(t, option) } func TestMedium_Store_Medium_Good(t *T) { - medium := newAX7Medium() + medium := newFixtureMedium() storeInstance, err := NewConfigured(StoreConfig{DatabasePath: testMemoryDatabasePath, Medium: medium}) RequireNoError(t, err) defer func() { _ = storeInstance.Close() }() @@ -597,14 +611,14 @@ func TestMedium_Store_Medium_Bad(t *T) { } func TestMedium_Store_Medium_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) medium := storeInstance.Medium() AssertNil(t, medium) } func TestMedium_Import_Good(t *T) { - _, workspace := ax7Workspace(t) - medium := newAX7Medium() + _, workspace := fixtureWorkspace(t) + medium := newFixtureMedium() RequireNoError(t, medium.Write(testRecordsJSONLFile, `{"name":"alice"}`)) err := Import(workspace, medium, testRecordsJSONLFile) AssertNoError(t, err) @@ -612,22 +626,22 @@ func TestMedium_Import_Good(t *T) { } func TestMedium_Import_Bad(t *T) { - medium := newAX7Medium() + medium := newFixtureMedium() err := Import(nil, medium, testRecordsJSONLFile) AssertError(t, err) } func TestMedium_Import_Ugly(t *T) { - _, workspace := ax7Workspace(t) - medium := newAX7Medium() + _, workspace := fixtureWorkspace(t) + medium := newFixtureMedium() RequireNoError(t, medium.Write("records.csv", "name\nalice\n")) err := Import(workspace, medium, "records.csv") AssertNoError(t, err) } func TestMedium_Export_Good(t *T) { - _, workspace := ax7Workspace(t) - medium := newAX7Medium() + _, workspace := fixtureWorkspace(t) + medium := newFixtureMedium() RequireNoError(t, workspace.Put("entry", map[string]any{"name": "alice"})) err := Export(workspace, medium, "out/report.json") AssertNoError(t, err) @@ -635,14 +649,14 @@ func TestMedium_Export_Good(t *T) { } func TestMedium_Export_Bad(t *T) { - medium := newAX7Medium() + medium := newFixtureMedium() err := Export(nil, medium, testReportJSONFile) AssertError(t, err) } func TestMedium_Export_Ugly(t *T) { - _, workspace := ax7Workspace(t) - medium := newAX7Medium() + _, workspace := fixtureWorkspace(t) + medium := newFixtureMedium() RequireNoError(t, workspace.Put("entry", map[string]any{"name": "alice"})) err := Export(workspace, medium, testReportJSONLFile) AssertNoError(t, err) diff --git a/parquet.go b/go/parquet.go similarity index 93% rename from parquet.go rename to go/parquet.go index a9ec756..87f1a6e 100644 --- a/parquet.go +++ b/go/parquet.go @@ -68,12 +68,12 @@ type ParquetRow struct { // Usage example: // // _, err := store.ExportParquet("/Volumes/Data/lem/training", "/Volumes/Data/lem/parquet") -func ExportParquet(trainingDir, outputDir string) (int, error) { - return 0, core.E( +func ExportParquet(trainingDir, outputDir string) (int, core.Result) { + return 0, core.Fail(core.E( "store.ExportParquet", "Parquet export requires an external tool so core does not ship a runtime Parquet dependency", nil, - ) + )) } // ExportSplitParquet reports that split-level Parquet export is intentionally @@ -82,10 +82,10 @@ func ExportParquet(trainingDir, outputDir string) (int, error) { // Usage example: // // _, err := store.ExportSplitParquet("/data/train.jsonl", "/data/parquet", "train") -func ExportSplitParquet(jsonlPath, outputDir, split string) (int, error) { - return 0, core.E( +func ExportSplitParquet(jsonlPath, outputDir, split string) (int, core.Result) { + return 0, core.Fail(core.E( "store.ExportSplitParquet", "Parquet export requires an external tool so core does not ship a runtime Parquet dependency", nil, - ) + )) } diff --git a/go/parquet_example_test.go b/go/parquet_example_test.go new file mode 100644 index 0000000..f139eba --- /dev/null +++ b/go/parquet_example_test.go @@ -0,0 +1,13 @@ +package store + +import core "dappco.re/go" + +func ExampleExportParquet() { + count, result := ExportParquet("training", "parquet") + core.Println(count, result.OK) +} + +func ExampleExportSplitParquet() { + count, result := ExportSplitParquet("training/train.jsonl", "parquet", "train") + core.Println(count, result.OK) +} diff --git a/parquet_test.go b/go/parquet_test.go similarity index 98% rename from parquet_test.go rename to go/parquet_test.go index 1c1190d..c1e7fc5 100644 --- a/parquet_test.go +++ b/go/parquet_test.go @@ -1,7 +1,6 @@ package store_test import ( - . "dappco.re/go" store "dappco.re/go/store" ) diff --git a/path_test.go b/go/path_test.go similarity index 100% rename from path_test.go rename to go/path_test.go diff --git a/publish.go b/go/publish.go similarity index 64% rename from publish.go rename to go/publish.go index da511a3..6bb3565 100644 --- a/publish.go +++ b/go/publish.go @@ -3,7 +3,6 @@ package store import ( - "bytes" "context" "io" "io/fs" @@ -79,9 +78,9 @@ type uploadEntry struct { // Usage example: // // err := store.Publish(store.PublishConfig{InputDir: "/data/parquet", Repo: "snider/lem-training"}, os.Stdout) -func Publish(cfg PublishConfig, w io.Writer) error { - if err := validatePublishConfig(cfg); err != nil { - return err +func Publish(cfg PublishConfig, w io.Writer) core.Result { + if result := validatePublishConfig(cfg); !result.OK { + return result } publishContext := cfg.Context @@ -91,15 +90,15 @@ func Publish(cfg PublishConfig, w io.Writer) error { token := resolveHFToken(cfg.Token) if token == "" && !cfg.DryRun { - return core.E(opPublish, "HuggingFace token required (--token, HF_TOKEN env, or ~/.huggingface/token)", nil) + return core.Fail(core.E(opPublish, "HuggingFace token required (--token, HF_TOKEN env, or ~/.huggingface/token)", nil)) } - files, hasSplit, err := collectUploadFiles(cfg.InputDir) - if err != nil { - return err + files, hasSplit, result := collectUploadFiles(cfg.InputDir) + if !result.OK { + return result } if !hasSplit { - return core.E(opPublish, core.Sprintf("no Parquet files found in %s", cfg.InputDir), nil) + return core.Fail(core.E(opPublish, core.Sprintf("no Parquet files found in %s", cfg.InputDir), nil)) } if cfg.DryRun { @@ -108,29 +107,30 @@ func Publish(cfg PublishConfig, w io.Writer) error { core.Print(w, "Publishing to https://huggingface.co/datasets/%s", cfg.Repo) - if err := ensureHFDatasetRepo(publishContext, token, cfg.Repo, cfg.Public); err != nil { - return core.E(opPublish, "ensure HuggingFace dataset", err) + if result := ensureHFDatasetRepo(publishContext, token, cfg.Repo, cfg.Public); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opPublish, "ensure HuggingFace dataset", err)) } - if err := uploadPublishFiles(publishContext, token, cfg.Repo, w, files); err != nil { - return err + if result := uploadPublishFiles(publishContext, token, cfg.Repo, w, files); !result.OK { + return result } core.Print(w, "\nPublished to https://huggingface.co/datasets/%s", cfg.Repo) - return nil + return core.Ok(nil) } -func validatePublishConfig(cfg PublishConfig) error { +func validatePublishConfig(cfg PublishConfig) core.Result { if cfg.InputDir == "" { - return core.E(opPublish, "input directory is required", nil) + return core.Fail(core.E(opPublish, "input directory is required", nil)) } if cfg.Repo == "" { - return core.E(opPublish, "repository is required", nil) + return core.Fail(core.E(opPublish, "repository is required", nil)) } - return nil + return core.Ok(nil) } -func publishDryRun(cfg PublishConfig, w io.Writer, files []uploadEntry) error { +func publishDryRun(cfg PublishConfig, w io.Writer, files []uploadEntry) core.Result { core.Print(w, "Dry run: would publish to %s", cfg.Repo) if cfg.Public { core.Print(w, " Visibility: public") @@ -140,23 +140,24 @@ func publishDryRun(cfg PublishConfig, w io.Writer, files []uploadEntry) error { for _, f := range files { statResult := localFs.Stat(f.local) if !statResult.OK { - return core.E(opPublish, core.Sprintf("stat %s", f.local), statResult.Value.(error)) + return core.Fail(core.E(opPublish, core.Sprintf("stat %s", f.local), statResult.Value.(error))) } info := statResult.Value.(fs.FileInfo) sizeMB := float64(info.Size()) / 1024 / 1024 core.Print(w, " %s -> %s (%.1f MB)", core.PathBase(f.local), f.remote, sizeMB) } - return nil + return core.Ok(nil) } -func uploadPublishFiles(ctx context.Context, token, repo string, w io.Writer, files []uploadEntry) error { +func uploadPublishFiles(ctx context.Context, token, repo string, w io.Writer, files []uploadEntry) core.Result { for _, f := range files { - if err := uploadFileToHF(ctx, token, repo, f.local, f.remote); err != nil { - return core.E(opPublish, core.Sprintf("upload %s", core.PathBase(f.local)), err) + if result := uploadFileToHF(ctx, token, repo, f.local, f.remote); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opPublish, core.Sprintf("upload %s", core.PathBase(f.local)), err)) } core.Print(w, " Uploaded %s -> %s", core.PathBase(f.local), f.remote) } - return nil + return core.Ok(nil) } // resolveHFToken returns a HuggingFace API token from the given value, @@ -191,7 +192,7 @@ func resolveHFToken(explicit string) string { } // collectUploadFiles finds Parquet split files and an optional dataset card. -func collectUploadFiles(inputDir string) ([]uploadEntry, bool, error) { +func collectUploadFiles(inputDir string) ([]uploadEntry, bool, core.Result) { splits := []string{"train", "valid", "test"} var files []uploadEntry hasSplit := false @@ -211,17 +212,17 @@ func collectUploadFiles(inputDir string) ([]uploadEntry, bool, error) { files = append(files, uploadEntry{cardPath, "README.md"}) } - return files, hasSplit, nil + return files, hasSplit, core.Ok(nil) } -func ensureHFDatasetRepo(ctx context.Context, token, repoID string, public bool) error { +func ensureHFDatasetRepo(ctx context.Context, token, repoID string, public bool) core.Result { if repoID == "" { - return core.E(opEnsureHFDatasetRepo, "repository is required", nil) + return core.Fail(core.E(opEnsureHFDatasetRepo, "repository is required", nil)) } organisation, name := splitHFRepoID(repoID) if name == "" { - return core.E(opEnsureHFDatasetRepo, "repository name is required", nil) + return core.Fail(core.E(opEnsureHFDatasetRepo, "repository name is required", nil)) } createPayload := map[string]any{ @@ -233,25 +234,27 @@ func ensureHFDatasetRepo(ctx context.Context, token, repoID string, public bool) createPayload["organization"] = organisation } - createStatus, createBody, err := hfJSONRequest(ctx, token, http.MethodPost, "https://huggingface.co/api/repos/create", createPayload) - if err != nil { - return core.E(opEnsureHFDatasetRepo, "create dataset repository", err) + createStatus, createBody, result := hfJSONRequest(ctx, token, http.MethodPost, "https://huggingface.co/api/repos/create", createPayload) + if !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opEnsureHFDatasetRepo, "create dataset repository", err)) } if createStatus >= 300 && createStatus != http.StatusConflict { - return core.E(opEnsureHFDatasetRepo, core.Sprintf("create dataset failed: HTTP %d: %s", createStatus, createBody), nil) + return core.Fail(core.E(opEnsureHFDatasetRepo, core.Sprintf("create dataset failed: HTTP %d: %s", createStatus, createBody), nil)) } settingsURL := core.Sprintf("https://huggingface.co/api/repos/dataset/%s/settings", repoID) - settingsStatus, settingsBody, err := hfJSONRequest(ctx, token, http.MethodPut, settingsURL, map[string]any{ + settingsStatus, settingsBody, result := hfJSONRequest(ctx, token, http.MethodPut, settingsURL, map[string]any{ "private": !public, }) - if err != nil { - return core.E(opEnsureHFDatasetRepo, "update dataset visibility", err) + if !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opEnsureHFDatasetRepo, "update dataset visibility", err)) } if settingsStatus >= 300 { - return core.E(opEnsureHFDatasetRepo, core.Sprintf("update dataset visibility failed: HTTP %d: %s", settingsStatus, settingsBody), nil) + return core.Fail(core.E(opEnsureHFDatasetRepo, core.Sprintf("update dataset visibility failed: HTTP %d: %s", settingsStatus, settingsBody), nil)) } - return nil + return core.Ok(nil) } func splitHFRepoID(repoID string) (organisation string, name string) { @@ -262,11 +265,11 @@ func splitHFRepoID(repoID string) (organisation string, name string) { return parts[0], parts[1] } -func hfJSONRequest(ctx context.Context, token, method, url string, payload map[string]any) (int, string, error) { +func hfJSONRequest(ctx context.Context, token, method, url string, payload map[string]any) (int, string, core.Result) { payloadJSON := core.JSONMarshalString(payload) - req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBufferString(payloadJSON)) + req, err := http.NewRequestWithContext(ctx, method, url, core.NewBufferString(payloadJSON)) if err != nil { - return 0, "", core.E(opHFJSONRequest, "create request", err) + return 0, "", core.Fail(core.E(opHFJSONRequest, "create request", err)) } req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") @@ -274,23 +277,23 @@ func hfJSONRequest(ctx context.Context, token, method, url string, payload map[s client := &http.Client{Timeout: 120 * time.Second} resp, err := client.Do(req) if err != nil { - return 0, "", core.E(opHFJSONRequest, "send request", err) + return 0, "", core.Fail(core.E(opHFJSONRequest, "send request", err)) } defer func() { _ = resp.Body.Close() }() body, err := io.ReadAll(resp.Body) if err != nil { - return resp.StatusCode, "", core.E(opHFJSONRequest, "read response body", err) + return resp.StatusCode, "", core.Fail(core.E(opHFJSONRequest, "read response body", err)) } - return resp.StatusCode, string(body), nil + return resp.StatusCode, string(body), core.Ok(nil) } // uploadFileToHF uploads a single file to a HuggingFace dataset repo via the // Hub API. -func uploadFileToHF(ctx context.Context, token, repoID, localPath, remotePath string) error { +func uploadFileToHF(ctx context.Context, token, repoID, localPath, remotePath string) core.Result { openResult := localFs.Open(localPath) if !openResult.OK { - return core.E(opUploadFileToHF, core.Sprintf(openPathFormat, localPath), openResult.Value.(error)) + return core.Fail(core.E(opUploadFileToHF, core.Sprintf(openPathFormat, localPath), openResult.Value.(error))) } file := openResult.Value.(fs.File) defer func() { _ = file.Close() }() @@ -299,7 +302,7 @@ func uploadFileToHF(ctx context.Context, token, repoID, localPath, remotePath st req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, file) if err != nil { - return core.E(opUploadFileToHF, "create request", err) + return core.Fail(core.E(opUploadFileToHF, "create request", err)) } req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/octet-stream") @@ -310,17 +313,17 @@ func uploadFileToHF(ctx context.Context, token, repoID, localPath, remotePath st client := &http.Client{} resp, err := client.Do(req) if err != nil { - return core.E(opUploadFileToHF, "upload request", err) + return core.Fail(core.E(opUploadFileToHF, "upload request", err)) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode >= 300 { body, readErr := io.ReadAll(resp.Body) if readErr != nil { - return core.E(opUploadFileToHF, "read error response body", readErr) + return core.Fail(core.E(opUploadFileToHF, "read error response body", readErr)) } - return core.E(opUploadFileToHF, core.Sprintf("upload failed: HTTP %d: %s", resp.StatusCode, string(body)), nil) + return core.Fail(core.E(opUploadFileToHF, core.Sprintf("upload failed: HTTP %d: %s", resp.StatusCode, string(body)), nil)) } - return nil + return core.Ok(nil) } diff --git a/go/publish_example_test.go b/go/publish_example_test.go new file mode 100644 index 0000000..9737a3c --- /dev/null +++ b/go/publish_example_test.go @@ -0,0 +1,13 @@ +package store + +import core "dappco.re/go" + +func ExamplePublish() { + buffer := core.NewBuffer() + result := Publish(PublishConfig{ + InputDir: "parquet", + Repo: "snider/lem-training", + DryRun: true, + }, buffer) + core.Println(result.OK) +} diff --git a/publish_test.go b/go/publish_test.go similarity index 89% rename from publish_test.go rename to go/publish_test.go index 141f0ed..9e1c2b0 100644 --- a/publish_test.go +++ b/go/publish_test.go @@ -1,16 +1,15 @@ package store import ( - "bytes" "testing" core "dappco.re/go" ) func TestPublish_Publish_Bad_EmptyRepository(t *testing.T) { - var output bytes.Buffer + output := core.NewBuffer() - err := Publish(PublishConfig{InputDir: t.TempDir(), DryRun: true}, &output) + err := Publish(PublishConfig{InputDir: t.TempDir(), DryRun: true}, output) assertError(t, err) assertContainsString(t, err.Error(), "repository is required") @@ -21,8 +20,8 @@ func TestPublish_Publish_Bad_DatasetCardWithoutParquetSplit(t *testing.T) { requireCoreOK(t, testFilesystem().EnsureDir(inputDir)) requireCoreWriteBytes(t, core.JoinPath(inputDir, "..", "dataset_card.md"), []byte("# Dataset\n")) - var output bytes.Buffer - err := Publish(PublishConfig{InputDir: inputDir, Repo: "snider/lem-training", DryRun: true}, &output) + output := core.NewBuffer() + err := Publish(PublishConfig{InputDir: inputDir, Repo: "snider/lem-training", DryRun: true}, output) assertError(t, err) assertContainsString(t, err.Error(), "no Parquet files found") @@ -43,7 +42,7 @@ func TestPublish_ResolveHFToken_Good_UserHomeFallback(t *testing.T) { func TestPublish_Publish_Good(t *T) { inputDir := Path(t.TempDir(), "data") - ax7WriteFile(t, Path(inputDir, "train.parquet"), "payload") + fixtureWriteFile(t, Path(inputDir, "train.parquet"), "payload") output := NewBuffer() err := Publish(PublishConfig{InputDir: inputDir, Repo: testHFDatasetID, DryRun: true}, output) AssertNoError(t, err) @@ -59,7 +58,7 @@ func TestPublish_Publish_Bad(t *T) { func TestPublish_Publish_Ugly(t *T) { inputDir := Path(t.TempDir(), "data") - ax7WriteFile(t, Path(inputDir, "valid.parquet"), "payload") + fixtureWriteFile(t, Path(inputDir, "valid.parquet"), "payload") output := NewBuffer() err := Publish(PublishConfig{InputDir: inputDir, Repo: testHFDatasetID, Public: true, DryRun: true}, output) AssertNoError(t, err) diff --git a/recover_test.go b/go/recover_test.go similarity index 100% rename from recover_test.go rename to go/recover_test.go diff --git a/scope.go b/go/scope.go similarity index 68% rename from scope.go rename to go/scope.go index cf51944..d41283e 100644 --- a/scope.go +++ b/go/scope.go @@ -25,15 +25,15 @@ type QuotaConfig struct { } // Usage example: `if err := (store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}).Validate(); err != nil { return }` -func (quotaConfig QuotaConfig) Validate() error { +func (quotaConfig QuotaConfig) Validate() core.Result { if quotaConfig.MaxKeys < 0 || quotaConfig.MaxGroups < 0 { - return core.E( + return core.Fail(core.E( "store.QuotaConfig.Validate", core.Sprintf("quota values must be zero or positive; got MaxKeys=%d MaxGroups=%d", quotaConfig.MaxKeys, quotaConfig.MaxGroups), nil, - ) + )) } - return nil + return core.Ok(nil) } // ScopedStoreConfig combines namespace selection with optional quota limits. @@ -46,18 +46,19 @@ type ScopedStoreConfig struct { } // Usage example: `if err := (store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}).Validate(); err != nil { return }` -func (scopedConfig ScopedStoreConfig) Validate() error { +func (scopedConfig ScopedStoreConfig) Validate() core.Result { if !validNamespace.MatchString(scopedConfig.Namespace) { - return core.E( + return core.Fail(core.E( "store.ScopedStoreConfig.Validate", core.Sprintf("namespace %q is invalid; use names like %q or %q", scopedConfig.Namespace, exampleTenantA, exampleTenant42), nil, - ) + )) } - if err := scopedConfig.Quota.Validate(); err != nil { - return core.E("store.ScopedStoreConfig.Validate", "quota", err) + if result := scopedConfig.Quota.Validate(); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E("store.ScopedStoreConfig.Validate", "quota", err)) } - return nil + return core.Ok(nil) } // Usage example: `scopedStore := store.NewScoped(storeInstance, "tenant-a")` @@ -99,26 +100,27 @@ func NewScoped(storeInstance *Store, namespace string) *ScopedStore { // Usage example: `scopedStore, err := store.NewScopedConfigured(storeInstance, store.ScopedStoreConfig{Namespace: "tenant-a", Quota: store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}}); if err != nil { return }` // This keeps the namespace and quota in one declarative literal instead of an // option chain. -func NewScopedConfigured(storeInstance *Store, scopedConfig ScopedStoreConfig) (*ScopedStore, error) { +func NewScopedConfigured(storeInstance *Store, scopedConfig ScopedStoreConfig) (*ScopedStore, core.Result) { if storeInstance == nil { - return nil, core.E(opNewScopedConfigured, "store instance is nil", nil) + return nil, core.Fail(core.E(opNewScopedConfigured, "store instance is nil", nil)) } - if err := scopedConfig.Validate(); err != nil { - return nil, core.E(opNewScopedConfigured, "validate config", err) + if result := scopedConfig.Validate(); !result.OK { + err, _ := result.Value.(error) + return nil, core.Fail(core.E(opNewScopedConfigured, "validate config", err)) } scopedStore := NewScoped(storeInstance, scopedConfig.Namespace) if scopedStore == nil { - return nil, core.E(opNewScopedConfigured, "construct scoped store", nil) + return nil, core.Fail(core.E(opNewScopedConfigured, "construct scoped store", nil)) } scopedStore.MaxKeys = scopedConfig.Quota.MaxKeys scopedStore.MaxGroups = scopedConfig.Quota.MaxGroups - return scopedStore, nil + return scopedStore, core.Ok(nil) } // Usage example: `scopedStore, err := store.NewScopedWithQuota(storeInstance, "tenant-a", store.QuotaConfig{MaxKeys: 100, MaxGroups: 10}); if err != nil { return }` // This is a convenience constructor for callers that already have the namespace // and quota values split across separate inputs. -func NewScopedWithQuota(storeInstance *Store, namespace string, quota QuotaConfig) (*ScopedStore, error) { +func NewScopedWithQuota(storeInstance *Store, namespace string, quota QuotaConfig) (*ScopedStore, core.Result) { return NewScopedConfigured(storeInstance, ScopedStoreConfig{ Namespace: namespace, Quota: quota, @@ -141,17 +143,17 @@ func (scopedStore *ScopedStore) trimNamespacePrefix(groupName string) string { return core.TrimPrefix(groupName, scopedStore.namespacePrefix()) } -func (scopedStore *ScopedStore) ensureReady(operation string) error { +func (scopedStore *ScopedStore) ensureReady(operation string) core.Result { if scopedStore == nil { - return core.E(operation, scopedStoreNilMessage, nil) + return core.Fail(core.E(operation, scopedStoreNilMessage, nil)) } if scopedStore.store == nil { - return core.E(operation, "scoped store store is nil", nil) + return core.Fail(core.E(operation, "scoped store store is nil", nil)) } - if err := scopedStore.store.ensureReady(operation); err != nil { - return err + if result := scopedStore.store.ensureReady(operation); !result.OK { + return result } - return nil + return core.Ok(nil) } // Namespace returns the namespace string. @@ -180,133 +182,137 @@ func (scopedStore *ScopedStore) Config() ScopedStoreConfig { // Usage example: `exists, err := scopedStore.Exists("colour")` // Usage example: `if exists, _ := scopedStore.Exists("token"); !exists { fmt.Println("session expired") }` -func (scopedStore *ScopedStore) Exists(key string) (bool, error) { - if err := scopedStore.ensureReady("store.ScopedStore.Exists"); err != nil { - return false, err +func (scopedStore *ScopedStore) Exists(key string) (bool, core.Result) { + if result := scopedStore.ensureReady("store.ScopedStore.Exists"); !result.OK { + return false, result } return scopedStore.store.Exists(scopedStore.namespacedGroup(scopedStore.defaultGroup()), key) } // Usage example: `exists, err := scopedStore.ExistsIn("config", "colour")` // Usage example: `if exists, _ := scopedStore.ExistsIn("session", "token"); !exists { fmt.Println("session expired") }` -func (scopedStore *ScopedStore) ExistsIn(group, key string) (bool, error) { - if err := scopedStore.ensureReady("store.ScopedStore.ExistsIn"); err != nil { - return false, err +func (scopedStore *ScopedStore) ExistsIn(group, key string) (bool, core.Result) { + if result := scopedStore.ensureReady("store.ScopedStore.ExistsIn"); !result.OK { + return false, result } return scopedStore.store.Exists(scopedStore.namespacedGroup(group), key) } // Usage example: `exists, err := scopedStore.GroupExists("config")` // Usage example: `if exists, _ := scopedStore.GroupExists("cache"); !exists { fmt.Println("group is empty") }` -func (scopedStore *ScopedStore) GroupExists(group string) (bool, error) { - if err := scopedStore.ensureReady("store.ScopedStore.GroupExists"); err != nil { - return false, err +func (scopedStore *ScopedStore) GroupExists(group string) (bool, core.Result) { + if result := scopedStore.ensureReady("store.ScopedStore.GroupExists"); !result.OK { + return false, result } return scopedStore.store.GroupExists(scopedStore.namespacedGroup(group)) } // Usage example: `colourValue, err := scopedStore.Get("colour")` -func (scopedStore *ScopedStore) Get(key string) (string, error) { - if err := scopedStore.ensureReady("store.ScopedStore.Get"); err != nil { - return "", err +func (scopedStore *ScopedStore) Get(key string) (string, core.Result) { + if result := scopedStore.ensureReady("store.ScopedStore.Get"); !result.OK { + return "", result } return scopedStore.store.Get(scopedStore.namespacedGroup(scopedStore.defaultGroup()), key) } // GetFrom reads a key from an explicit namespaced group. // Usage example: `colourValue, err := scopedStore.GetFrom("config", "colour")` -func (scopedStore *ScopedStore) GetFrom(group, key string) (string, error) { - if err := scopedStore.ensureReady("store.ScopedStore.GetFrom"); err != nil { - return "", err +func (scopedStore *ScopedStore) GetFrom(group, key string) (string, core.Result) { + if result := scopedStore.ensureReady("store.ScopedStore.GetFrom"); !result.OK { + return "", result } return scopedStore.store.Get(scopedStore.namespacedGroup(group), key) } // Usage example: `if err := scopedStore.Set("colour", "blue"); err != nil { return }` -func (scopedStore *ScopedStore) Set(key, value string) error { - if err := scopedStore.ensureReady("store.ScopedStore.Set"); err != nil { - return err +func (scopedStore *ScopedStore) Set(key, value string) core.Result { + if result := scopedStore.ensureReady("store.ScopedStore.Set"); !result.OK { + return result } - if err := scopedStore.Transaction(func(scopedTransaction *ScopedStoreTransaction) error { + if result := scopedStore.Transaction(func(scopedTransaction *ScopedStoreTransaction) core.Result { return scopedTransaction.Set(key, value) - }); err != nil { - return core.E("store.ScopedStore.Set", "write scoped key", err) + }); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E("store.ScopedStore.Set", "write scoped key", err)) } - return nil + return core.Ok(nil) } // SetIn writes a key to an explicit namespaced group. // Usage example: `if err := scopedStore.SetIn("config", "colour", "blue"); err != nil { return }` -func (scopedStore *ScopedStore) SetIn(group, key, value string) error { - if err := scopedStore.ensureReady("store.ScopedStore.SetIn"); err != nil { - return err +func (scopedStore *ScopedStore) SetIn(group, key, value string) core.Result { + if result := scopedStore.ensureReady("store.ScopedStore.SetIn"); !result.OK { + return result } - if err := scopedStore.Transaction(func(scopedTransaction *ScopedStoreTransaction) error { + if result := scopedStore.Transaction(func(scopedTransaction *ScopedStoreTransaction) core.Result { return scopedTransaction.SetIn(group, key, value) - }); err != nil { - return core.E("store.ScopedStore.SetIn", "write scoped group key", err) + }); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E("store.ScopedStore.SetIn", "write scoped group key", err)) } - return nil + return core.Ok(nil) } // Usage example: `if err := scopedStore.SetWithTTL("sessions", "token", "abc123", time.Hour); err != nil { return }` -func (scopedStore *ScopedStore) SetWithTTL(group, key, value string, timeToLive time.Duration) error { - if err := scopedStore.ensureReady("store.ScopedStore.SetWithTTL"); err != nil { - return err +func (scopedStore *ScopedStore) SetWithTTL(group, key, value string, timeToLive time.Duration) core.Result { + if result := scopedStore.ensureReady("store.ScopedStore.SetWithTTL"); !result.OK { + return result } - if err := scopedStore.Transaction(func(scopedTransaction *ScopedStoreTransaction) error { + if result := scopedStore.Transaction(func(scopedTransaction *ScopedStoreTransaction) core.Result { return scopedTransaction.SetWithTTL(group, key, value, timeToLive) - }); err != nil { - return core.E("store.ScopedStore.SetWithTTL", "write scoped group key with TTL", err) + }); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E("store.ScopedStore.SetWithTTL", "write scoped group key with TTL", err)) } - return nil + return core.Ok(nil) } // Usage example: `if err := scopedStore.Delete("config", "colour"); err != nil { return }` -func (scopedStore *ScopedStore) Delete(group, key string) error { - if err := scopedStore.ensureReady("store.ScopedStore.Delete"); err != nil { - return err +func (scopedStore *ScopedStore) Delete(group, key string) core.Result { + if result := scopedStore.ensureReady("store.ScopedStore.Delete"); !result.OK { + return result } return scopedStore.store.Delete(scopedStore.namespacedGroup(group), key) } // Usage example: `if err := scopedStore.DeleteGroup("cache"); err != nil { return }` -func (scopedStore *ScopedStore) DeleteGroup(group string) error { - if err := scopedStore.ensureReady("store.ScopedStore.DeleteGroup"); err != nil { - return err +func (scopedStore *ScopedStore) DeleteGroup(group string) core.Result { + if result := scopedStore.ensureReady("store.ScopedStore.DeleteGroup"); !result.OK { + return result } return scopedStore.store.DeleteGroup(scopedStore.namespacedGroup(group)) } // Usage example: `if err := scopedStore.DeletePrefix("cache"); err != nil { return }` // Usage example: `if err := scopedStore.DeletePrefix(""); err != nil { return }` -func (scopedStore *ScopedStore) DeletePrefix(groupPrefix string) error { - if err := scopedStore.ensureReady("store.ScopedStore.DeletePrefix"); err != nil { - return err +func (scopedStore *ScopedStore) DeletePrefix(groupPrefix string) core.Result { + if result := scopedStore.ensureReady("store.ScopedStore.DeletePrefix"); !result.OK { + return result } return scopedStore.store.DeletePrefix(scopedStore.namespacedGroup(groupPrefix)) } // Usage example: `colourEntries, err := scopedStore.GetAll("config")` -func (scopedStore *ScopedStore) GetAll(group string) (map[string]string, error) { - if err := scopedStore.ensureReady("store.ScopedStore.GetAll"); err != nil { - return nil, err +func (scopedStore *ScopedStore) GetAll(group string) (map[string]string, core.Result) { + if result := scopedStore.ensureReady("store.ScopedStore.GetAll"); !result.OK { + return nil, result } return scopedStore.store.GetAll(scopedStore.namespacedGroup(group)) } // Usage example: `page, err := scopedStore.GetPage("config", 0, 25); if err != nil { return }; for _, entry := range page { fmt.Println(entry.Key, entry.Value) }` -func (scopedStore *ScopedStore) GetPage(group string, offset, limit int) ([]KeyValue, error) { - if err := scopedStore.ensureReady("store.ScopedStore.GetPage"); err != nil { - return nil, err +func (scopedStore *ScopedStore) GetPage(group string, offset, limit int) ([]KeyValue, core.Result) { + if result := scopedStore.ensureReady("store.ScopedStore.GetPage"); !result.OK { + return nil, result } return scopedStore.store.GetPage(scopedStore.namespacedGroup(group), offset, limit) } // Usage example: `for entry, err := range scopedStore.All("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }` func (scopedStore *ScopedStore) All(group string) iter.Seq2[KeyValue, error] { - if err := scopedStore.ensureReady("store.ScopedStore.All"); err != nil { + if result := scopedStore.ensureReady("store.ScopedStore.All"); !result.OK { return func(yield func(KeyValue, error) bool) { + err, _ := result.Value.(error) yield(KeyValue{}, err) } } @@ -319,43 +325,44 @@ func (scopedStore *ScopedStore) AllSeq(group string) iter.Seq2[KeyValue, error] } // Usage example: `keyCount, err := scopedStore.Count("config")` -func (scopedStore *ScopedStore) Count(group string) (int, error) { - if err := scopedStore.ensureReady("store.ScopedStore.Count"); err != nil { - return 0, err +func (scopedStore *ScopedStore) Count(group string) (int, core.Result) { + if result := scopedStore.ensureReady("store.ScopedStore.Count"); !result.OK { + return 0, result } return scopedStore.store.Count(scopedStore.namespacedGroup(group)) } // Usage example: `keyCount, err := scopedStore.CountAll("config")` // Usage example: `keyCount, err := scopedStore.CountAll()` -func (scopedStore *ScopedStore) CountAll(groupPrefix ...string) (int, error) { - if err := scopedStore.ensureReady("store.ScopedStore.CountAll"); err != nil { - return 0, err +func (scopedStore *ScopedStore) CountAll(groupPrefix ...string) (int, core.Result) { + if result := scopedStore.ensureReady("store.ScopedStore.CountAll"); !result.OK { + return 0, result } return scopedStore.store.CountAll(scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix))) } // Usage example: `groupNames, err := scopedStore.Groups("config")` // Usage example: `groupNames, err := scopedStore.Groups()` -func (scopedStore *ScopedStore) Groups(groupPrefix ...string) ([]string, error) { - if err := scopedStore.ensureReady("store.ScopedStore.Groups"); err != nil { - return nil, err +func (scopedStore *ScopedStore) Groups(groupPrefix ...string) ([]string, core.Result) { + if result := scopedStore.ensureReady("store.ScopedStore.Groups"); !result.OK { + return nil, result } - groupNames, err := scopedStore.store.Groups(scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix))) - if err != nil { - return nil, err + groupNames, result := scopedStore.store.Groups(scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix))) + if !result.OK { + return nil, result } for i, groupName := range groupNames { groupNames[i] = scopedStore.trimNamespacePrefix(groupName) } - return groupNames, nil + return groupNames, core.Ok(nil) } // Usage example: `for groupName, err := range scopedStore.GroupsSeq("config") { if err != nil { break }; fmt.Println(groupName) }` // Usage example: `for groupName, err := range scopedStore.GroupsSeq() { if err != nil { break }; fmt.Println(groupName) }` func (scopedStore *ScopedStore) GroupsSeq(groupPrefix ...string) iter.Seq2[string, error] { return func(yield func(string, error) bool) { - if err := scopedStore.ensureReady("store.ScopedStore.GroupsSeq"); err != nil { + if result := scopedStore.ensureReady("store.ScopedStore.GroupsSeq"); !result.OK { + err, _ := result.Value.(error) yield("", err) return } @@ -375,42 +382,43 @@ func (scopedStore *ScopedStore) GroupsSeq(groupPrefix ...string) iter.Seq2[strin } // Usage example: `renderedTemplate, err := scopedStore.Render("Hello {{ .name }}", "user")` -func (scopedStore *ScopedStore) Render(templateSource, group string) (string, error) { - if err := scopedStore.ensureReady("store.ScopedStore.Render"); err != nil { - return "", err +func (scopedStore *ScopedStore) Render(templateSource, group string) (string, core.Result) { + if result := scopedStore.ensureReady("store.ScopedStore.Render"); !result.OK { + return "", result } return scopedStore.store.Render(templateSource, scopedStore.namespacedGroup(group)) } // Usage example: `parts, err := scopedStore.GetSplit("config", "hosts", ","); if err != nil { return }; for part := range parts { fmt.Println(part) }` -func (scopedStore *ScopedStore) GetSplit(group, key, separator string) (iter.Seq[string], error) { - if err := scopedStore.ensureReady("store.ScopedStore.GetSplit"); err != nil { - return nil, err +func (scopedStore *ScopedStore) GetSplit(group, key, separator string) (iter.Seq[string], core.Result) { + if result := scopedStore.ensureReady("store.ScopedStore.GetSplit"); !result.OK { + return nil, result } return scopedStore.store.GetSplit(scopedStore.namespacedGroup(group), key, separator) } // Usage example: `fields, err := scopedStore.GetFields("config", "flags"); if err != nil { return }; for field := range fields { fmt.Println(field) }` -func (scopedStore *ScopedStore) GetFields(group, key string) (iter.Seq[string], error) { - if err := scopedStore.ensureReady("store.ScopedStore.GetFields"); err != nil { - return nil, err +func (scopedStore *ScopedStore) GetFields(group, key string) (iter.Seq[string], core.Result) { + if result := scopedStore.ensureReady("store.ScopedStore.GetFields"); !result.OK { + return nil, result } return scopedStore.store.GetFields(scopedStore.namespacedGroup(group), key) } // Usage example: `removedRows, err := scopedStore.PurgeExpired(); if err != nil { return }; fmt.Println(removedRows)` -func (scopedStore *ScopedStore) PurgeExpired() (int64, error) { - if err := scopedStore.ensureReady("store.ScopedStore.PurgeExpired"); err != nil { - return 0, err +func (scopedStore *ScopedStore) PurgeExpired() (int64, core.Result) { + if result := scopedStore.ensureReady("store.ScopedStore.PurgeExpired"); !result.OK { + return 0, result } cutoffUnixMilli := time.Now().UnixMilli() - expiredEntries, err := deleteExpiredEntriesMatchingGroupPrefix(scopedStore.store.sqliteDatabase, scopedStore.namespacePrefix(), cutoffUnixMilli) - if err != nil { - return 0, core.E("store.ScopedStore.PurgeExpired", "delete expired rows", err) + expiredEntries, result := deleteExpiredEntriesMatchingGroupPrefix(scopedStore.store.sqliteDatabase, scopedStore.namespacePrefix(), cutoffUnixMilli) + if !result.OK { + err, _ := result.Value.(error) + return 0, core.Fail(core.E("store.ScopedStore.PurgeExpired", "delete expired rows", err)) } scopedStore.store.notifyExpiredEntries(expiredEntries) - return int64(len(expiredEntries)), nil + return int64(len(expiredEntries)), core.Ok(nil) } // Usage example: `events := scopedStore.Watch("config")` @@ -552,18 +560,18 @@ type ScopedStoreTransaction struct { } // Usage example: `err := scopedStore.Transaction(func(transaction *store.ScopedStoreTransaction) error { return transaction.Set("theme", "dark") })` -func (scopedStore *ScopedStore) Transaction(operation func(*ScopedStoreTransaction) error) error { +func (scopedStore *ScopedStore) Transaction(operation func(*ScopedStoreTransaction) core.Result) core.Result { if scopedStore == nil { - return core.E(opScopedStoreTransaction, scopedStoreNilMessage, nil) + return core.Fail(core.E(opScopedStoreTransaction, scopedStoreNilMessage, nil)) } if operation == nil { - return core.E(opScopedStoreTransaction, "operation is nil", nil) + return core.Fail(core.E(opScopedStoreTransaction, "operation is nil", nil)) } if scopedStore.store == nil { - return core.E(opScopedStoreTransaction, "scoped store store is nil", nil) + return core.Fail(core.E(opScopedStoreTransaction, "scoped store store is nil", nil)) } - return scopedStore.store.Transaction(func(storeTransaction *StoreTransaction) error { + return scopedStore.store.Transaction(func(storeTransaction *StoreTransaction) core.Result { return operation(&ScopedStoreTransaction{ scopedStore: scopedStore, storeTransaction: storeTransaction, @@ -571,26 +579,26 @@ func (scopedStore *ScopedStore) Transaction(operation func(*ScopedStoreTransacti }) } -func (scopedStoreTransaction *ScopedStoreTransaction) ensureReady(operation string) error { +func (scopedStoreTransaction *ScopedStoreTransaction) ensureReady(operation string) core.Result { if scopedStoreTransaction == nil { - return core.E(operation, "scoped transaction is nil", nil) + return core.Fail(core.E(operation, "scoped transaction is nil", nil)) } if scopedStoreTransaction.scopedStore == nil { - return core.E(operation, "scoped transaction store is nil", nil) + return core.Fail(core.E(operation, "scoped transaction store is nil", nil)) } if scopedStoreTransaction.storeTransaction == nil { - return core.E(operation, "scoped transaction database is nil", nil) + return core.Fail(core.E(operation, "scoped transaction database is nil", nil)) } - if err := scopedStoreTransaction.scopedStore.store.ensureReady(operation); err != nil { - return err + if result := scopedStoreTransaction.scopedStore.store.ensureReady(operation); !result.OK { + return result } return scopedStoreTransaction.storeTransaction.ensureReady(operation) } // Usage example: `exists, err := scopedStoreTransaction.Exists("colour")` -func (scopedStoreTransaction *ScopedStoreTransaction) Exists(key string) (bool, error) { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Exists"); err != nil { - return false, err +func (scopedStoreTransaction *ScopedStoreTransaction) Exists(key string) (bool, core.Result) { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Exists"); !result.OK { + return false, result } return scopedStoreTransaction.storeTransaction.Exists( scopedStoreTransaction.scopedStore.namespacedGroup(scopedStoreTransaction.scopedStore.defaultGroup()), @@ -599,25 +607,25 @@ func (scopedStoreTransaction *ScopedStoreTransaction) Exists(key string) (bool, } // Usage example: `exists, err := scopedStoreTransaction.ExistsIn("config", "colour")` -func (scopedStoreTransaction *ScopedStoreTransaction) ExistsIn(group, key string) (bool, error) { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.ExistsIn"); err != nil { - return false, err +func (scopedStoreTransaction *ScopedStoreTransaction) ExistsIn(group, key string) (bool, core.Result) { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.ExistsIn"); !result.OK { + return false, result } return scopedStoreTransaction.storeTransaction.Exists(scopedStoreTransaction.scopedStore.namespacedGroup(group), key) } // Usage example: `exists, err := scopedStoreTransaction.GroupExists("config")` -func (scopedStoreTransaction *ScopedStoreTransaction) GroupExists(group string) (bool, error) { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GroupExists"); err != nil { - return false, err +func (scopedStoreTransaction *ScopedStoreTransaction) GroupExists(group string) (bool, core.Result) { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GroupExists"); !result.OK { + return false, result } return scopedStoreTransaction.storeTransaction.GroupExists(scopedStoreTransaction.scopedStore.namespacedGroup(group)) } // Usage example: `colourValue, err := scopedStoreTransaction.Get("colour")` -func (scopedStoreTransaction *ScopedStoreTransaction) Get(key string) (string, error) { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Get"); err != nil { - return "", err +func (scopedStoreTransaction *ScopedStoreTransaction) Get(key string) (string, core.Result) { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Get"); !result.OK { + return "", result } return scopedStoreTransaction.storeTransaction.Get( scopedStoreTransaction.scopedStore.namespacedGroup(scopedStoreTransaction.scopedStore.defaultGroup()), @@ -626,21 +634,21 @@ func (scopedStoreTransaction *ScopedStoreTransaction) Get(key string) (string, e } // Usage example: `colourValue, err := scopedStoreTransaction.GetFrom("config", "colour")` -func (scopedStoreTransaction *ScopedStoreTransaction) GetFrom(group, key string) (string, error) { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GetFrom"); err != nil { - return "", err +func (scopedStoreTransaction *ScopedStoreTransaction) GetFrom(group, key string) (string, core.Result) { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GetFrom"); !result.OK { + return "", result } return scopedStoreTransaction.storeTransaction.Get(scopedStoreTransaction.scopedStore.namespacedGroup(group), key) } // Usage example: `if err := scopedStoreTransaction.Set("theme", "dark"); err != nil { return err }` -func (scopedStoreTransaction *ScopedStoreTransaction) Set(key, value string) error { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Set"); err != nil { - return err +func (scopedStoreTransaction *ScopedStoreTransaction) Set(key, value string) core.Result { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Set"); !result.OK { + return result } defaultGroup := scopedStoreTransaction.scopedStore.defaultGroup() - if err := scopedStoreTransaction.checkQuota("store.ScopedStoreTransaction.Set", defaultGroup, key); err != nil { - return err + if result := scopedStoreTransaction.checkQuota("store.ScopedStoreTransaction.Set", defaultGroup, key); !result.OK { + return result } return scopedStoreTransaction.storeTransaction.Set( scopedStoreTransaction.scopedStore.namespacedGroup(defaultGroup), @@ -650,72 +658,73 @@ func (scopedStoreTransaction *ScopedStoreTransaction) Set(key, value string) err } // Usage example: `if err := scopedStoreTransaction.SetIn("config", "colour", "blue"); err != nil { return err }` -func (scopedStoreTransaction *ScopedStoreTransaction) SetIn(group, key, value string) error { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.SetIn"); err != nil { - return err +func (scopedStoreTransaction *ScopedStoreTransaction) SetIn(group, key, value string) core.Result { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.SetIn"); !result.OK { + return result } - if err := scopedStoreTransaction.checkQuota("store.ScopedStoreTransaction.SetIn", group, key); err != nil { - return err + if result := scopedStoreTransaction.checkQuota("store.ScopedStoreTransaction.SetIn", group, key); !result.OK { + return result } return scopedStoreTransaction.storeTransaction.Set(scopedStoreTransaction.scopedStore.namespacedGroup(group), key, value) } // Usage example: `if err := scopedStoreTransaction.SetWithTTL("sessions", "token", "abc123", time.Hour); err != nil { return err }` -func (scopedStoreTransaction *ScopedStoreTransaction) SetWithTTL(group, key, value string, timeToLive time.Duration) error { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.SetWithTTL"); err != nil { - return err +func (scopedStoreTransaction *ScopedStoreTransaction) SetWithTTL(group, key, value string, timeToLive time.Duration) core.Result { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.SetWithTTL"); !result.OK { + return result } - if err := scopedStoreTransaction.checkQuota("store.ScopedStoreTransaction.SetWithTTL", group, key); err != nil { - return err + if result := scopedStoreTransaction.checkQuota("store.ScopedStoreTransaction.SetWithTTL", group, key); !result.OK { + return result } return scopedStoreTransaction.storeTransaction.SetWithTTL(scopedStoreTransaction.scopedStore.namespacedGroup(group), key, value, timeToLive) } // Usage example: `if err := scopedStoreTransaction.Delete("config", "colour"); err != nil { return err }` -func (scopedStoreTransaction *ScopedStoreTransaction) Delete(group, key string) error { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Delete"); err != nil { - return err +func (scopedStoreTransaction *ScopedStoreTransaction) Delete(group, key string) core.Result { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Delete"); !result.OK { + return result } return scopedStoreTransaction.storeTransaction.Delete(scopedStoreTransaction.scopedStore.namespacedGroup(group), key) } // Usage example: `if err := scopedStoreTransaction.DeleteGroup("cache"); err != nil { return err }` -func (scopedStoreTransaction *ScopedStoreTransaction) DeleteGroup(group string) error { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.DeleteGroup"); err != nil { - return err +func (scopedStoreTransaction *ScopedStoreTransaction) DeleteGroup(group string) core.Result { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.DeleteGroup"); !result.OK { + return result } return scopedStoreTransaction.storeTransaction.DeleteGroup(scopedStoreTransaction.scopedStore.namespacedGroup(group)) } // Usage example: `if err := scopedStoreTransaction.DeletePrefix("cache"); err != nil { return err }` // Usage example: `if err := scopedStoreTransaction.DeletePrefix(""); err != nil { return err }` -func (scopedStoreTransaction *ScopedStoreTransaction) DeletePrefix(groupPrefix string) error { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.DeletePrefix"); err != nil { - return err +func (scopedStoreTransaction *ScopedStoreTransaction) DeletePrefix(groupPrefix string) core.Result { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.DeletePrefix"); !result.OK { + return result } return scopedStoreTransaction.storeTransaction.DeletePrefix(scopedStoreTransaction.scopedStore.namespacedGroup(groupPrefix)) } // Usage example: `colourEntries, err := scopedStoreTransaction.GetAll("config")` -func (scopedStoreTransaction *ScopedStoreTransaction) GetAll(group string) (map[string]string, error) { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GetAll"); err != nil { - return nil, err +func (scopedStoreTransaction *ScopedStoreTransaction) GetAll(group string) (map[string]string, core.Result) { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GetAll"); !result.OK { + return nil, result } return scopedStoreTransaction.storeTransaction.GetAll(scopedStoreTransaction.scopedStore.namespacedGroup(group)) } // Usage example: `page, err := scopedStoreTransaction.GetPage("config", 0, 25); if err != nil { return }; for _, entry := range page { fmt.Println(entry.Key, entry.Value) }` -func (scopedStoreTransaction *ScopedStoreTransaction) GetPage(group string, offset, limit int) ([]KeyValue, error) { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GetPage"); err != nil { - return nil, err +func (scopedStoreTransaction *ScopedStoreTransaction) GetPage(group string, offset, limit int) ([]KeyValue, core.Result) { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GetPage"); !result.OK { + return nil, result } return scopedStoreTransaction.storeTransaction.GetPage(scopedStoreTransaction.scopedStore.namespacedGroup(group), offset, limit) } // Usage example: `for entry, err := range scopedStoreTransaction.All("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }` func (scopedStoreTransaction *ScopedStoreTransaction) All(group string) iter.Seq2[KeyValue, error] { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.All"); err != nil { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.All"); !result.OK { return func(yield func(KeyValue, error) bool) { + err, _ := result.Value.(error) yield(KeyValue{}, err) } } @@ -728,44 +737,45 @@ func (scopedStoreTransaction *ScopedStoreTransaction) AllSeq(group string) iter. } // Usage example: `keyCount, err := scopedStoreTransaction.Count("config")` -func (scopedStoreTransaction *ScopedStoreTransaction) Count(group string) (int, error) { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Count"); err != nil { - return 0, err +func (scopedStoreTransaction *ScopedStoreTransaction) Count(group string) (int, core.Result) { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Count"); !result.OK { + return 0, result } return scopedStoreTransaction.storeTransaction.Count(scopedStoreTransaction.scopedStore.namespacedGroup(group)) } // Usage example: `keyCount, err := scopedStoreTransaction.CountAll("config")` // Usage example: `keyCount, err := scopedStoreTransaction.CountAll()` -func (scopedStoreTransaction *ScopedStoreTransaction) CountAll(groupPrefix ...string) (int, error) { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.CountAll"); err != nil { - return 0, err +func (scopedStoreTransaction *ScopedStoreTransaction) CountAll(groupPrefix ...string) (int, core.Result) { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.CountAll"); !result.OK { + return 0, result } return scopedStoreTransaction.storeTransaction.CountAll(scopedStoreTransaction.scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix))) } // Usage example: `groupNames, err := scopedStoreTransaction.Groups("config")` // Usage example: `groupNames, err := scopedStoreTransaction.Groups()` -func (scopedStoreTransaction *ScopedStoreTransaction) Groups(groupPrefix ...string) ([]string, error) { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Groups"); err != nil { - return nil, err +func (scopedStoreTransaction *ScopedStoreTransaction) Groups(groupPrefix ...string) ([]string, core.Result) { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Groups"); !result.OK { + return nil, result } - groupNames, err := scopedStoreTransaction.storeTransaction.Groups(scopedStoreTransaction.scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix))) - if err != nil { - return nil, err + groupNames, result := scopedStoreTransaction.storeTransaction.Groups(scopedStoreTransaction.scopedStore.namespacedGroup(firstStringOrEmpty(groupPrefix))) + if !result.OK { + return nil, result } for i, groupName := range groupNames { groupNames[i] = scopedStoreTransaction.scopedStore.trimNamespacePrefix(groupName) } - return groupNames, nil + return groupNames, core.Ok(nil) } // Usage example: `for groupName, err := range scopedStoreTransaction.GroupsSeq("config") { if err != nil { break }; fmt.Println(groupName) }` // Usage example: `for groupName, err := range scopedStoreTransaction.GroupsSeq() { if err != nil { break }; fmt.Println(groupName) }` func (scopedStoreTransaction *ScopedStoreTransaction) GroupsSeq(groupPrefix ...string) iter.Seq2[string, error] { return func(yield func(string, error) bool) { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GroupsSeq"); err != nil { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GroupsSeq"); !result.OK { + err, _ := result.Value.(error) yield("", err) return } @@ -786,45 +796,46 @@ func (scopedStoreTransaction *ScopedStoreTransaction) GroupsSeq(groupPrefix ...s } // Usage example: `renderedTemplate, err := scopedStoreTransaction.Render("Hello {{ .name }}", "user")` -func (scopedStoreTransaction *ScopedStoreTransaction) Render(templateSource, group string) (string, error) { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Render"); err != nil { - return "", err +func (scopedStoreTransaction *ScopedStoreTransaction) Render(templateSource, group string) (string, core.Result) { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.Render"); !result.OK { + return "", result } return scopedStoreTransaction.storeTransaction.Render(templateSource, scopedStoreTransaction.scopedStore.namespacedGroup(group)) } // Usage example: `parts, err := scopedStoreTransaction.GetSplit("config", "hosts", ","); if err != nil { return }; for part := range parts { fmt.Println(part) }` -func (scopedStoreTransaction *ScopedStoreTransaction) GetSplit(group, key, separator string) (iter.Seq[string], error) { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GetSplit"); err != nil { - return nil, err +func (scopedStoreTransaction *ScopedStoreTransaction) GetSplit(group, key, separator string) (iter.Seq[string], core.Result) { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GetSplit"); !result.OK { + return nil, result } return scopedStoreTransaction.storeTransaction.GetSplit(scopedStoreTransaction.scopedStore.namespacedGroup(group), key, separator) } // Usage example: `fields, err := scopedStoreTransaction.GetFields("config", "flags"); if err != nil { return }; for field := range fields { fmt.Println(field) }` -func (scopedStoreTransaction *ScopedStoreTransaction) GetFields(group, key string) (iter.Seq[string], error) { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GetFields"); err != nil { - return nil, err +func (scopedStoreTransaction *ScopedStoreTransaction) GetFields(group, key string) (iter.Seq[string], core.Result) { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.GetFields"); !result.OK { + return nil, result } return scopedStoreTransaction.storeTransaction.GetFields(scopedStoreTransaction.scopedStore.namespacedGroup(group), key) } // Usage example: `removedRows, err := scopedStoreTransaction.PurgeExpired(); if err != nil { return err }; fmt.Println(removedRows)` -func (scopedStoreTransaction *ScopedStoreTransaction) PurgeExpired() (int64, error) { - if err := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.PurgeExpired"); err != nil { - return 0, err +func (scopedStoreTransaction *ScopedStoreTransaction) PurgeExpired() (int64, core.Result) { + if result := scopedStoreTransaction.ensureReady("store.ScopedStoreTransaction.PurgeExpired"); !result.OK { + return 0, result } cutoffUnixMilli := time.Now().UnixMilli() - expiredEntries, err := deleteExpiredEntriesMatchingGroupPrefix(scopedStoreTransaction.storeTransaction.sqliteTransaction, scopedStoreTransaction.scopedStore.namespacePrefix(), cutoffUnixMilli) - if err != nil { - return 0, core.E("store.ScopedStoreTransaction.PurgeExpired", "delete expired rows", err) + expiredEntries, result := deleteExpiredEntriesMatchingGroupPrefix(scopedStoreTransaction.storeTransaction.sqliteTransaction, scopedStoreTransaction.scopedStore.namespacePrefix(), cutoffUnixMilli) + if !result.OK { + err, _ := result.Value.(error) + return 0, core.Fail(core.E("store.ScopedStoreTransaction.PurgeExpired", "delete expired rows", err)) } scopedStoreTransaction.storeTransaction.recordExpiredEntries(expiredEntries) - return int64(len(expiredEntries)), nil + return int64(len(expiredEntries)), core.Ok(nil) } -func (scopedStoreTransaction *ScopedStoreTransaction) checkQuota(operation, group, key string) error { +func (scopedStoreTransaction *ScopedStoreTransaction) checkQuota(operation, group, key string) core.Result { return enforceQuota(quotaCheck{ operation: operation, key: key, @@ -838,8 +849,8 @@ func (scopedStoreTransaction *ScopedStoreTransaction) checkQuota(operation, grou } type quotaCounter interface { - CountAll(groupPrefix string) (int, error) - Count(group string) (int, error) + CountAll(groupPrefix string) (int, core.Result) + Count(group string) (int, core.Result) GroupsSeq(groupPrefix ...string) iter.Seq2[string, error] } @@ -854,72 +865,76 @@ type quotaCheck struct { counter quotaCounter } -func enforceQuota(check quotaCheck) error { +func enforceQuota(check quotaCheck) core.Result { if check.maxKeys == 0 && check.maxGroups == 0 { - return nil + return core.Ok(nil) } - exists, err := liveEntryExists(check.queryable, check.namespacedGroup, check.key) - if err != nil { - return core.E(check.operation, quotaCheckContext, err) + exists, result := liveEntryExists(check.queryable, check.namespacedGroup, check.key) + if !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(check.operation, quotaCheckContext, err)) } if exists { - return nil + return core.Ok(nil) } - if err := enforceKeyQuota(check); err != nil { - return err + if result := enforceKeyQuota(check); !result.OK { + return result } return enforceGroupQuota(check) } -func enforceKeyQuota(check quotaCheck) error { +func enforceKeyQuota(check quotaCheck) core.Result { if check.maxKeys <= 0 { - return nil + return core.Ok(nil) } - keyCount, err := check.counter.CountAll(check.namespacePrefix) - if err != nil { - return core.E(check.operation, quotaCheckContext, err) + keyCount, result := check.counter.CountAll(check.namespacePrefix) + if !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(check.operation, quotaCheckContext, err)) } if keyCount >= check.maxKeys { - return core.E(check.operation, core.Sprintf("key limit (%d)", check.maxKeys), QuotaExceededError) + return core.Fail(core.E(check.operation, core.Sprintf("key limit (%d)", check.maxKeys), QuotaExceededError)) } - return nil + return core.Ok(nil) } -func enforceGroupQuota(check quotaCheck) error { +func enforceGroupQuota(check quotaCheck) core.Result { if check.maxGroups <= 0 { - return nil + return core.Ok(nil) } - existingGroupCount, err := check.counter.Count(check.namespacedGroup) - if err != nil { - return core.E(check.operation, quotaCheckContext, err) + existingGroupCount, result := check.counter.Count(check.namespacedGroup) + if !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(check.operation, quotaCheckContext, err)) } if existingGroupCount > 0 { - return nil + return core.Ok(nil) } - knownGroupCount, err := countKnownGroups(check.counter, check.namespacePrefix) - if err != nil { - return core.E(check.operation, quotaCheckContext, err) + knownGroupCount, result := countKnownGroups(check.counter, check.namespacePrefix) + if !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(check.operation, quotaCheckContext, err)) } if knownGroupCount >= check.maxGroups { - return core.E(check.operation, core.Sprintf("group limit (%d)", check.maxGroups), QuotaExceededError) + return core.Fail(core.E(check.operation, core.Sprintf("group limit (%d)", check.maxGroups), QuotaExceededError)) } - return nil + return core.Ok(nil) } -func countKnownGroups(counter quotaCounter, namespacePrefix string) (int, error) { +func countKnownGroups(counter quotaCounter, namespacePrefix string) (int, core.Result) { knownGroupCount := 0 for _, iterationErr := range counter.GroupsSeq(namespacePrefix) { if iterationErr != nil { - return 0, iterationErr + return 0, core.Fail(iterationErr) } knownGroupCount++ } - return knownGroupCount, nil + return knownGroupCount, core.Ok(nil) } -func liveEntryExists(queryable keyExistenceQuery, group, key string) (bool, error) { +func liveEntryExists(queryable keyExistenceQuery, group, key string) (bool, core.Result) { var exists int err := queryable.QueryRow( "SELECT 1 FROM "+entriesTableName+sqlWhere+entryGroupColumn+" = ? AND "+entryKeyColumn+" = ? AND (expires_at IS NULL OR expires_at > ?) LIMIT 1", @@ -928,12 +943,12 @@ func liveEntryExists(queryable keyExistenceQuery, group, key string) (bool, erro time.Now().UnixMilli(), ).Scan(&exists) if err == nil { - return true, nil + return true, core.Ok(nil) } if err == sql.ErrNoRows { - return false, nil + return false, core.Ok(nil) } - return false, err + return false, core.Fail(err) } type keyExistenceQuery interface { diff --git a/go/scope_example_test.go b/go/scope_example_test.go new file mode 100644 index 0000000..cc3af4a --- /dev/null +++ b/go/scope_example_test.go @@ -0,0 +1,551 @@ +package store + +import ( + "time" + + core "dappco.re/go" +) + +func exampleScopedStore() (*Store, *ScopedStore) { + storeInstance := exampleOpenStore() + scopedStore := NewScoped(storeInstance, "tenant-a") + return storeInstance, scopedStore +} + +func exampleScopedStoreWithData() (*Store, *ScopedStore) { + storeInstance, scopedStore := exampleScopedStore() + exampleRequireOK(scopedStore.Set("colour", "blue")) + exampleRequireOK(scopedStore.SetIn("config", "tags", "alpha,beta")) + return storeInstance, scopedStore +} + +func ExampleQuotaConfig_Validate() { + config := QuotaConfig{MaxKeys: 10, MaxGroups: 2} + result := config.Validate() + core.Println(result.OK) +} + +func ExampleScopedStoreConfig_Validate() { + config := ScopedStoreConfig{Namespace: "tenant-a", Quota: QuotaConfig{MaxKeys: 10}} + result := config.Validate() + core.Println(result.OK) +} + +func ExampleNewScoped() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + scopedStore := NewScoped(storeInstance, "tenant-a") + core.Println(scopedStore.Namespace()) +} + +func ExampleNewScopedConfigured() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + scopedStore, result := NewScopedConfigured(storeInstance, ScopedStoreConfig{Namespace: "tenant-a"}) + exampleRequireOK(result) + core.Println(scopedStore.Namespace()) +} + +func ExampleNewScopedWithQuota() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + scopedStore, result := NewScopedWithQuota(storeInstance, "tenant-a", QuotaConfig{MaxKeys: 10}) + exampleRequireOK(result) + core.Println(scopedStore.Config().Quota.MaxKeys) +} + +func ExampleScopedStore_Namespace() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + core.Println(scopedStore.Namespace()) +} + +func ExampleScopedStore_Config() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + core.Println(scopedStore.Config().Namespace) +} + +func ExampleScopedStore_Exists() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + exists, result := scopedStore.Exists("colour") + exampleRequireOK(result) + core.Println(exists) +} + +func ExampleScopedStore_ExistsIn() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + exists, result := scopedStore.ExistsIn("config", "tags") + exampleRequireOK(result) + core.Println(exists) +} + +func ExampleScopedStore_GroupExists() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + exists, result := scopedStore.GroupExists("config") + exampleRequireOK(result) + core.Println(exists) +} + +func ExampleScopedStore_Get() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + value, result := scopedStore.Get("colour") + exampleRequireOK(result) + core.Println(value) +} + +func ExampleScopedStore_GetFrom() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + value, result := scopedStore.GetFrom("config", "tags") + exampleRequireOK(result) + core.Println(value) +} + +func ExampleScopedStore_Set() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Set("colour", "blue") + exampleRequireOK(result) +} + +func ExampleScopedStore_SetIn() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.SetIn("config", "colour", "blue") + exampleRequireOK(result) +} + +func ExampleScopedStore_SetWithTTL() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.SetWithTTL(defaultScopedGroupName, "token", "abc", time.Minute) + exampleRequireOK(result) +} + +func ExampleScopedStore_Delete() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + result := scopedStore.Delete(defaultScopedGroupName, "colour") + exampleRequireOK(result) +} + +func ExampleScopedStore_DeleteGroup() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + result := scopedStore.DeleteGroup("config") + exampleRequireOK(result) +} + +func ExampleScopedStore_DeletePrefix() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + result := scopedStore.DeletePrefix("conf") + exampleRequireOK(result) +} + +func ExampleScopedStore_GetAll() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + values, result := scopedStore.GetAll("config") + exampleRequireOK(result) + core.Println(values["tags"]) +} + +func ExampleScopedStore_GetPage() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + entries, result := scopedStore.GetPage("config", 0, 10) + exampleRequireOK(result) + core.Println(len(entries)) +} + +func ExampleScopedStore_All() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + entries := exampleCollectKeyValues(scopedStore.All("config")) + core.Println(len(entries)) +} + +func ExampleScopedStore_AllSeq() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + entries := exampleCollectKeyValues(scopedStore.AllSeq("config")) + core.Println(len(entries)) +} + +func ExampleScopedStore_Count() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + count, result := scopedStore.Count("config") + exampleRequireOK(result) + core.Println(count) +} + +func ExampleScopedStore_CountAll() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + count, result := scopedStore.CountAll("conf") + exampleRequireOK(result) + core.Println(count) +} + +func ExampleScopedStore_Groups() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + groups, result := scopedStore.Groups() + exampleRequireOK(result) + core.Println(groups) +} + +func ExampleScopedStore_GroupsSeq() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + groups := exampleCollectGroups(scopedStore.GroupsSeq()) + core.Println(groups) +} + +func ExampleScopedStore_Render() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + rendered, result := scopedStore.Render("tags={{.tags}}", "config") + exampleRequireOK(result) + core.Println(rendered) +} + +func ExampleScopedStore_GetSplit() { + storeInstance, scopedStore := exampleScopedStoreWithData() + defer exampleCloseStore(storeInstance) + seq, result := scopedStore.GetSplit("config", "tags", ",") + exampleRequireOK(result) + core.Println(exampleCollectStrings(seq)) +} + +func ExampleScopedStore_GetFields() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + exampleRequireOK(scopedStore.SetIn("config", "text", "alpha beta")) + seq, result := scopedStore.GetFields("config", "text") + exampleRequireOK(result) + core.Println(exampleCollectStrings(seq)) +} + +func ExampleScopedStore_PurgeExpired() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + exampleRequireOK(scopedStore.SetWithTTL(defaultScopedGroupName, "token", "abc", time.Nanosecond)) + removed, result := scopedStore.PurgeExpired() + exampleRequireOK(result) + core.Println(removed) +} + +func ExampleScopedStore_Watch() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + events := scopedStore.Watch("config") + scopedStore.Unwatch("config", events) +} + +func ExampleScopedStore_Unwatch() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + events := scopedStore.Watch("config") + scopedStore.Unwatch("config", events) +} + +func ExampleScopedStore_OnChange() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + unregister := scopedStore.OnChange(func(event Event) { + core.Println(event.Group) + }) + unregister() +} + +func ExampleScopedStore_Transaction() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + return transaction.Set("colour", "blue") + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_Exists() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.Set("colour", "blue")) + exists, existsResult := transaction.Exists("colour") + exampleRequireOK(existsResult) + core.Println(exists) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_ExistsIn() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "colour", "blue")) + exists, existsResult := transaction.ExistsIn("config", "colour") + exampleRequireOK(existsResult) + core.Println(exists) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_GroupExists() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "colour", "blue")) + exists, existsResult := transaction.GroupExists("config") + exampleRequireOK(existsResult) + core.Println(exists) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_Get() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.Set("colour", "blue")) + value, getResult := transaction.Get("colour") + exampleRequireOK(getResult) + core.Println(value) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_GetFrom() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "colour", "blue")) + value, getResult := transaction.GetFrom("config", "colour") + exampleRequireOK(getResult) + core.Println(value) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_Set() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + return transaction.Set("colour", "blue") + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_SetIn() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + return transaction.SetIn("config", "colour", "blue") + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_SetWithTTL() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + return transaction.SetWithTTL(defaultScopedGroupName, "token", "abc", time.Minute) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_Delete() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "colour", "blue")) + return transaction.Delete("config", "colour") + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_DeleteGroup() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "colour", "blue")) + return transaction.DeleteGroup("config") + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_DeletePrefix() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "colour", "blue")) + return transaction.DeletePrefix("conf") + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_GetAll() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "colour", "blue")) + values, valuesResult := transaction.GetAll("config") + exampleRequireOK(valuesResult) + core.Println(values["colour"]) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_GetPage() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "colour", "blue")) + entries, entriesResult := transaction.GetPage("config", 0, 10) + exampleRequireOK(entriesResult) + core.Println(len(entries)) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_All() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "colour", "blue")) + entries := exampleCollectKeyValues(transaction.All("config")) + core.Println(len(entries)) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_AllSeq() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "colour", "blue")) + entries := exampleCollectKeyValues(transaction.AllSeq("config")) + core.Println(len(entries)) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_Count() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "colour", "blue")) + count, countResult := transaction.Count("config") + exampleRequireOK(countResult) + core.Println(count) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_CountAll() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "colour", "blue")) + count, countResult := transaction.CountAll("conf") + exampleRequireOK(countResult) + core.Println(count) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_Groups() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "colour", "blue")) + groups, groupsResult := transaction.Groups() + exampleRequireOK(groupsResult) + core.Println(groups) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_GroupsSeq() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "colour", "blue")) + groups := exampleCollectGroups(transaction.GroupsSeq()) + core.Println(groups) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_Render() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "colour", "blue")) + rendered, renderResult := transaction.Render("colour={{.colour}}", "config") + exampleRequireOK(renderResult) + core.Println(rendered) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_GetSplit() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "tags", "alpha,beta")) + seq, splitResult := transaction.GetSplit("config", "tags", ",") + exampleRequireOK(splitResult) + core.Println(exampleCollectStrings(seq)) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_GetFields() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetIn("config", "text", "alpha beta")) + seq, fieldsResult := transaction.GetFields("config", "text") + exampleRequireOK(fieldsResult) + core.Println(exampleCollectStrings(seq)) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleScopedStoreTransaction_PurgeExpired() { + storeInstance, scopedStore := exampleScopedStore() + defer exampleCloseStore(storeInstance) + result := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + exampleRequireOK(transaction.SetWithTTL(defaultScopedGroupName, "token", "abc", time.Nanosecond)) + removed, purgeResult := transaction.PurgeExpired() + exampleRequireOK(purgeResult) + core.Println(removed) + return core.Ok(nil) + }) + exampleRequireOK(result) +} diff --git a/scope_test.go b/go/scope_test.go similarity index 88% rename from scope_test.go rename to go/scope_test.go index 6b34e6b..631dbc4 100644 --- a/scope_test.go +++ b/go/scope_test.go @@ -248,7 +248,15 @@ func TestScope_ScopedStore_Good_DefaultGroupHelpers(t *testing.T) { } func TestScope_ScopedStore_Good_SetInGetFrom(t *testing.T) { - assertScopedStoreSetInGetFrom(t) + storeInstance, _ := New(testMemoryDatabasePath) + defer func() { _ = storeInstance.Close() }() + + scopedStore := NewScoped(storeInstance, testTenantA) + assertNoError(t, scopedStore.SetIn("config", "theme", "dark")) + + value, err := scopedStore.GetFrom("config", "theme") + assertNoError(t, err) + assertEqual(t, "dark", value) } func assertScopedStoreSetInGetFrom(t *testing.T) { @@ -279,7 +287,7 @@ func TestScope_ScopedStore_Good_PrefixedInUnderlyingStore(t *testing.T) { // Direct access without prefix should fail. _, err = storeInstance.Get("config", "key") - assertTrue(t, core.Is(err, NotFoundError)) + assertTrue(t, errIs(errorValue(err), NotFoundError)) } func TestScope_ScopedStore_Good_NamespaceIsolation(t *testing.T) { @@ -407,7 +415,7 @@ func TestScope_ScopedStore_Good_Delete(t *testing.T) { assertNoError(t, scopedStore.Delete("g", "k")) _, err := scopedStore.GetFrom("g", "k") - assertTrue(t, core.Is(err, NotFoundError)) + assertTrue(t, errIs(errorValue(err), NotFoundError)) } func TestScope_ScopedStore_Good_DeleteGroup(t *testing.T) { @@ -439,9 +447,9 @@ func TestScope_ScopedStore_Good_DeletePrefix(t *testing.T) { assertNoError(t, scopedStore.DeletePrefix("cache")) _, err := scopedStore.GetFrom("cache", "page") - assertTrue(t, core.Is(err, NotFoundError)) + assertTrue(t, errIs(errorValue(err), NotFoundError)) _, err = scopedStore.GetFrom("cache-warm", "status") - assertTrue(t, core.Is(err, NotFoundError)) + assertTrue(t, errIs(errorValue(err), NotFoundError)) value, err := scopedStore.GetFrom("config", "theme") assertNoError(t, err) @@ -669,7 +677,7 @@ func TestScope_ScopedStore_Good_SetWithTTL_Expires(t *testing.T) { time.Sleep(5 * time.Millisecond) _, err := scopedStore.GetFrom("g", "k") - assertTrue(t, core.Is(err, NotFoundError)) + assertTrue(t, errIs(errorValue(err), NotFoundError)) } func TestScope_ScopedStore_Good_Render(t *testing.T) { @@ -803,7 +811,7 @@ func TestScope_ScopedStore_Good_PurgeExpired(t *testing.T) { assertEqual(t, int64(1), removedRows) _, err = scopedStore.GetFrom("session", "token") - assertTrue(t, core.Is(err, NotFoundError)) + assertTrue(t, errIs(errorValue(err), NotFoundError)) } func TestScope_ScopedStore_Good_PurgeExpired_NamespaceLocal(t *testing.T) { @@ -850,7 +858,7 @@ func TestScope_Quota_Good_MaxKeys(t *testing.T) { // 6th key should fail. err = scopedStore.SetIn("g", "overflow", "v") assertError(t, err) - assertTruef(t, core.Is(err, QuotaExceededError), "expected QuotaExceededError, got: %v", err) + assertTruef(t, errIs(errorValue(err), QuotaExceededError), "expected QuotaExceededError, got: %v", err) } func TestScope_Quota_Bad_QuotaCheckQueryError(t *testing.T) { @@ -888,7 +896,7 @@ func TestScope_Quota_Good_MaxKeys_AcrossGroups(t *testing.T) { // Total is now 3 — any new key should fail regardless of group. err := scopedStore.SetIn("g4", "d", "4") - assertTrue(t, core.Is(err, QuotaExceededError)) + assertTrue(t, errIs(errorValue(err), QuotaExceededError)) } func TestScope_Quota_Good_UpsertDoesNotCount(t *testing.T) { @@ -1006,7 +1014,7 @@ func TestScope_Quota_Good_ExpiredKeysExcluded(t *testing.T) { // Now at 3 — next should fail. err := scopedStore.SetIn("g", "new3", "v") - assertTrue(t, core.Is(err, QuotaExceededError)) + assertTrue(t, errIs(errorValue(err), QuotaExceededError)) } func TestScope_Quota_Good_SetWithTTL_Enforced(t *testing.T) { @@ -1022,7 +1030,7 @@ func TestScope_Quota_Good_SetWithTTL_Enforced(t *testing.T) { assertNoError(t, scopedStore.SetWithTTL("g", "b", "2", time.Hour)) err := scopedStore.SetWithTTL("g", "c", "3", time.Hour) - assertTrue(t, core.Is(err, QuotaExceededError)) + assertTrue(t, errIs(errorValue(err), QuotaExceededError)) } // --------------------------------------------------------------------------- @@ -1045,7 +1053,7 @@ func TestScope_Quota_Good_MaxGroups(t *testing.T) { // 4th group should fail. err := scopedStore.SetIn("g4", "k", "v") assertError(t, err) - assertTrue(t, core.Is(err, QuotaExceededError)) + assertTrue(t, errIs(errorValue(err), QuotaExceededError)) } func TestScope_Quota_Good_MaxGroups_ExistingGroupOK(t *testing.T) { @@ -1129,7 +1137,7 @@ func TestScope_Quota_Good_BothLimits(t *testing.T) { // Group limit hit. err := scopedStore.SetIn("g3", "c", "3") - assertTrue(t, core.Is(err, QuotaExceededError)) + assertTrue(t, errIs(errorValue(err), QuotaExceededError)) // But adding to existing groups is fine (within key limit). assertNoError(t, scopedStore.SetIn("g1", "d", "4")) @@ -1155,11 +1163,11 @@ func TestScope_Quota_Good_DoesNotAffectOtherNamespaces(t *testing.T) { // alphaStore is at limit — but betaStore's keys don't count against alphaStore. err := alphaStore.SetIn("g", "a3", "v") - assertTrue(t, core.Is(err, QuotaExceededError)) + assertTrue(t, errIs(errorValue(err), QuotaExceededError)) // betaStore is also at limit independently. err = betaStore.SetIn("g", "b3", "v") - assertTrue(t, core.Is(err, QuotaExceededError)) + assertTrue(t, errIs(errorValue(err), QuotaExceededError)) } // --------------------------------------------------------------------------- @@ -1403,7 +1411,7 @@ func TestScope_NewScoped_Bad(t *T) { } func TestScope_NewScoped_Ugly(t *T) { - scopedStore := NewScoped(ax7Store(t), testTenant42) + scopedStore := NewScoped(fixtureStore(t), testTenant42) AssertNotNil(t, scopedStore) AssertEqual(t, testTenant42, scopedStore.Namespace()) } @@ -1415,7 +1423,7 @@ func TestScope_NewScopedConfigured_Bad(t *T) { } func TestScope_NewScopedConfigured_Ugly(t *T) { - scopedStore, err := NewScopedConfigured(ax7Store(t), ScopedStoreConfig{Namespace: testTenantA, Quota: QuotaConfig{MaxKeys: 1, MaxGroups: 1}}) + scopedStore, err := NewScopedConfigured(fixtureStore(t), ScopedStoreConfig{Namespace: testTenantA, Quota: QuotaConfig{MaxKeys: 1, MaxGroups: 1}}) RequireNoError(t, err) AssertEqual(t, 1, scopedStore.Config().Quota.MaxKeys) } @@ -1427,13 +1435,13 @@ func TestScope_NewScopedWithQuota_Bad(t *T) { } func TestScope_NewScopedWithQuota_Ugly(t *T) { - scopedStore, err := NewScopedWithQuota(ax7Store(t), testTenantA, QuotaConfig{MaxGroups: 1}) + scopedStore, err := NewScopedWithQuota(fixtureStore(t), testTenantA, QuotaConfig{MaxGroups: 1}) RequireNoError(t, err) AssertEqual(t, 1, scopedStore.Config().Quota.MaxGroups) } func TestScope_ScopedStore_Namespace_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) namespace := scopedStore.Namespace() AssertEqual(t, testTenantA, namespace) } @@ -1445,13 +1453,13 @@ func TestScope_ScopedStore_Namespace_Bad(t *T) { } func TestScope_ScopedStore_Namespace_Ugly(t *T) { - scopedStore := ax7QuotaScopedStore(t, 1, 1) + scopedStore := fixtureQuotaScopedStore(t, 1, 1) namespace := scopedStore.Namespace() AssertEqual(t, testTenantA, namespace) } func TestScope_ScopedStore_Config_Good(t *T) { - scopedStore := ax7QuotaScopedStore(t, 2, 1) + scopedStore := fixtureQuotaScopedStore(t, 2, 1) config := scopedStore.Config() AssertEqual(t, 2, config.Quota.MaxKeys) } @@ -1463,13 +1471,13 @@ func TestScope_ScopedStore_Config_Bad(t *T) { } func TestScope_ScopedStore_Config_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) config := scopedStore.Config() AssertEqual(t, testTenantA, config.Namespace) } func TestScope_ScopedStore_Exists_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.Set("colour", "blue")) exists, err := scopedStore.Exists("colour") AssertNoError(t, err) @@ -1484,14 +1492,14 @@ func TestScope_ScopedStore_Exists_Bad(t *T) { } func TestScope_ScopedStore_Exists_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) exists, err := scopedStore.Exists("missing") AssertNoError(t, err) AssertFalse(t, exists) } func TestScope_ScopedStore_ExistsIn_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) exists, err := scopedStore.ExistsIn("config", "colour") AssertNoError(t, err) @@ -1506,14 +1514,14 @@ func TestScope_ScopedStore_ExistsIn_Bad(t *T) { } func TestScope_ScopedStore_ExistsIn_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) exists, err := scopedStore.ExistsIn("config", "missing") AssertNoError(t, err) AssertFalse(t, exists) } func TestScope_ScopedStore_GroupExists_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) exists, err := scopedStore.GroupExists("config") AssertNoError(t, err) @@ -1528,14 +1536,14 @@ func TestScope_ScopedStore_GroupExists_Bad(t *T) { } func TestScope_ScopedStore_GroupExists_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) exists, err := scopedStore.GroupExists("missing") AssertNoError(t, err) AssertFalse(t, exists) } func TestScope_ScopedStore_Get_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.Set("colour", "blue")) got, err := scopedStore.Get("colour") AssertNoError(t, err) @@ -1550,14 +1558,14 @@ func TestScope_ScopedStore_Get_Bad(t *T) { } func TestScope_ScopedStore_Get_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) got, err := scopedStore.Get("missing") AssertErrorIs(t, err, NotFoundError) AssertEqual(t, "", got) } func TestScope_ScopedStore_GetFrom_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) got, err := scopedStore.GetFrom("config", "colour") AssertNoError(t, err) @@ -1572,17 +1580,17 @@ func TestScope_ScopedStore_GetFrom_Bad(t *T) { } func TestScope_ScopedStore_GetFrom_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) got, err := scopedStore.GetFrom("config", "missing") AssertErrorIs(t, err, NotFoundError) AssertEqual(t, "", got) } func TestScope_ScopedStore_Set_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) err := scopedStore.Set("colour", "blue") AssertNoError(t, err) - AssertTrue(t, ax7ScopedExists(t, scopedStore, "colour")) + AssertTrue(t, fixtureScopedExists(t, scopedStore, "colour")) } func TestScope_ScopedStore_Set_Bad(t *T) { @@ -1592,17 +1600,17 @@ func TestScope_ScopedStore_Set_Bad(t *T) { } func TestScope_ScopedStore_Set_Ugly(t *T) { - scopedStore := ax7QuotaScopedStore(t, 1, 0) + scopedStore := fixtureQuotaScopedStore(t, 1, 0) RequireNoError(t, scopedStore.Set("colour", "blue")) err := scopedStore.Set("shape", "circle") AssertErrorIs(t, err, QuotaExceededError) } func TestScope_ScopedStore_SetIn_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) err := scopedStore.SetIn("config", "colour", "blue") AssertNoError(t, err) - AssertTrue(t, ax7ScopedExistsIn(t, scopedStore, "config", "colour")) + AssertTrue(t, fixtureScopedExistsIn(t, scopedStore, "config", "colour")) } func TestScope_ScopedStore_SetIn_Bad(t *T) { @@ -1612,17 +1620,17 @@ func TestScope_ScopedStore_SetIn_Bad(t *T) { } func TestScope_ScopedStore_SetIn_Ugly(t *T) { - scopedStore := ax7QuotaScopedStore(t, 0, 1) + scopedStore := fixtureQuotaScopedStore(t, 0, 1) RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) err := scopedStore.SetIn("other", "shape", "circle") AssertErrorIs(t, err, QuotaExceededError) } func TestScope_ScopedStore_SetWithTTL_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) err := scopedStore.SetWithTTL("sessions", "token", "abc", Hour) AssertNoError(t, err) - AssertTrue(t, ax7ScopedExistsIn(t, scopedStore, "sessions", "token")) + AssertTrue(t, fixtureScopedExistsIn(t, scopedStore, "sessions", "token")) } func TestScope_ScopedStore_SetWithTTL_Bad(t *T) { @@ -1632,7 +1640,7 @@ func TestScope_ScopedStore_SetWithTTL_Bad(t *T) { } func TestScope_ScopedStore_SetWithTTL_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetWithTTL("sessions", "token", "abc", -Millisecond)) exists, err := scopedStore.ExistsIn("sessions", "token") AssertNoError(t, err) @@ -1640,11 +1648,11 @@ func TestScope_ScopedStore_SetWithTTL_Ugly(t *T) { } func TestScope_ScopedStore_Delete_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) err := scopedStore.Delete("config", "colour") AssertNoError(t, err) - AssertFalse(t, ax7ScopedExistsIn(t, scopedStore, "config", "colour")) + AssertFalse(t, fixtureScopedExistsIn(t, scopedStore, "config", "colour")) } func TestScope_ScopedStore_Delete_Bad(t *T) { @@ -1654,18 +1662,18 @@ func TestScope_ScopedStore_Delete_Bad(t *T) { } func TestScope_ScopedStore_Delete_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) err := scopedStore.Delete("missing", "key") AssertNoError(t, err) - AssertFalse(t, ax7ScopedExistsIn(t, scopedStore, "missing", "key")) + AssertFalse(t, fixtureScopedExistsIn(t, scopedStore, "missing", "key")) } func TestScope_ScopedStore_DeleteGroup_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) err := scopedStore.DeleteGroup("config") AssertNoError(t, err) - AssertFalse(t, ax7ScopedGroupExists(t, scopedStore, "config")) + AssertFalse(t, fixtureScopedGroupExists(t, scopedStore, "config")) } func TestScope_ScopedStore_DeleteGroup_Bad(t *T) { @@ -1675,18 +1683,18 @@ func TestScope_ScopedStore_DeleteGroup_Bad(t *T) { } func TestScope_ScopedStore_DeleteGroup_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) err := scopedStore.DeleteGroup("missing") AssertNoError(t, err) - AssertFalse(t, ax7ScopedGroupExists(t, scopedStore, "missing")) + AssertFalse(t, fixtureScopedGroupExists(t, scopedStore, "missing")) } func TestScope_ScopedStore_DeletePrefix_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn(testCacheA, "k", "v")) err := scopedStore.DeletePrefix("cache") AssertNoError(t, err) - AssertFalse(t, ax7ScopedGroupExists(t, scopedStore, testCacheA)) + AssertFalse(t, fixtureScopedGroupExists(t, scopedStore, testCacheA)) } func TestScope_ScopedStore_DeletePrefix_Bad(t *T) { @@ -1696,15 +1704,15 @@ func TestScope_ScopedStore_DeletePrefix_Bad(t *T) { } func TestScope_ScopedStore_DeletePrefix_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("cache", "k", "v")) err := scopedStore.DeletePrefix("") AssertNoError(t, err) - AssertFalse(t, ax7ScopedGroupExists(t, scopedStore, "cache")) + AssertFalse(t, fixtureScopedGroupExists(t, scopedStore, "cache")) } func TestScope_ScopedStore_GetAll_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) entries, err := scopedStore.GetAll("config") AssertNoError(t, err) @@ -1719,14 +1727,14 @@ func TestScope_ScopedStore_GetAll_Bad(t *T) { } func TestScope_ScopedStore_GetAll_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) entries, err := scopedStore.GetAll("missing") AssertNoError(t, err) AssertEmpty(t, entries) } func TestScope_ScopedStore_GetPage_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) page, err := scopedStore.GetPage("config", 0, 1) AssertNoError(t, err) @@ -1741,58 +1749,58 @@ func TestScope_ScopedStore_GetPage_Bad(t *T) { } func TestScope_ScopedStore_GetPage_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) page, err := scopedStore.GetPage("missing", 0, 1) AssertNoError(t, err) AssertEmpty(t, page) } func TestScope_ScopedStore_All_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) - entries, err := ax7CollectKeyValues(scopedStore.All("config")) + entries, err := fixtureCollectKeyValues(scopedStore.All("config")) AssertNoError(t, err) AssertEqual(t, "colour", entries[0].Key) } func TestScope_ScopedStore_All_Bad(t *T) { var scopedStore *ScopedStore - entries, err := ax7CollectKeyValues(scopedStore.All("config")) + entries, err := fixtureCollectKeyValues(scopedStore.All("config")) AssertError(t, err) AssertEmpty(t, entries) } func TestScope_ScopedStore_All_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - entries, err := ax7CollectKeyValues(scopedStore.All("missing")) + scopedStore := fixtureScopedStore(t) + entries, err := fixtureCollectKeyValues(scopedStore.All("missing")) AssertNoError(t, err) AssertEmpty(t, entries) } func TestScope_ScopedStore_AllSeq_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) - entries, err := ax7CollectKeyValues(scopedStore.AllSeq("config")) + entries, err := fixtureCollectKeyValues(scopedStore.AllSeq("config")) AssertNoError(t, err) AssertEqual(t, "blue", entries[0].Value) } func TestScope_ScopedStore_AllSeq_Bad(t *T) { var scopedStore *ScopedStore - entries, err := ax7CollectKeyValues(scopedStore.AllSeq("config")) + entries, err := fixtureCollectKeyValues(scopedStore.AllSeq("config")) AssertError(t, err) AssertEmpty(t, entries) } func TestScope_ScopedStore_AllSeq_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - entries, err := ax7CollectKeyValues(scopedStore.AllSeq("missing")) + scopedStore := fixtureScopedStore(t) + entries, err := fixtureCollectKeyValues(scopedStore.AllSeq("missing")) AssertNoError(t, err) AssertEmpty(t, entries) } func TestScope_ScopedStore_Count_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) count, err := scopedStore.Count("config") AssertNoError(t, err) @@ -1807,14 +1815,14 @@ func TestScope_ScopedStore_Count_Bad(t *T) { } func TestScope_ScopedStore_Count_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) count, err := scopedStore.Count("missing") AssertNoError(t, err) AssertEqual(t, 0, count) } func TestScope_ScopedStore_CountAll_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) count, err := scopedStore.CountAll() AssertNoError(t, err) @@ -1829,14 +1837,14 @@ func TestScope_ScopedStore_CountAll_Bad(t *T) { } func TestScope_ScopedStore_CountAll_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) count, err := scopedStore.CountAll("missing") AssertNoError(t, err) AssertEqual(t, 0, count) } func TestScope_ScopedStore_Groups_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) groups, err := scopedStore.Groups() AssertNoError(t, err) @@ -1851,36 +1859,36 @@ func TestScope_ScopedStore_Groups_Bad(t *T) { } func TestScope_ScopedStore_Groups_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) groups, err := scopedStore.Groups("missing") AssertNoError(t, err) AssertEmpty(t, groups) } func TestScope_ScopedStore_GroupsSeq_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) - groups, err := ax7CollectGroups(scopedStore.GroupsSeq()) + groups, err := fixtureCollectGroups(scopedStore.GroupsSeq()) AssertNoError(t, err) AssertEqual(t, []string{"config"}, groups) } func TestScope_ScopedStore_GroupsSeq_Bad(t *T) { var scopedStore *ScopedStore - groups, err := ax7CollectGroups(scopedStore.GroupsSeq()) + groups, err := fixtureCollectGroups(scopedStore.GroupsSeq()) AssertError(t, err) AssertEmpty(t, groups) } func TestScope_ScopedStore_GroupsSeq_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - groups, err := ax7CollectGroups(scopedStore.GroupsSeq("missing")) + scopedStore := fixtureScopedStore(t) + groups, err := fixtureCollectGroups(scopedStore.GroupsSeq("missing")) AssertNoError(t, err) AssertEmpty(t, groups) } func TestScope_ScopedStore_Render_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "name", "alice")) rendered, err := scopedStore.Render("hello {{ .name }}", "config") AssertNoError(t, err) @@ -1895,18 +1903,18 @@ func TestScope_ScopedStore_Render_Bad(t *T) { } func TestScope_ScopedStore_Render_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) rendered, err := scopedStore.Render("empty", "missing") AssertNoError(t, err) AssertEqual(t, "empty", rendered) } func TestScope_ScopedStore_GetSplit_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "hosts", "a,b")) seq, err := scopedStore.GetSplit("config", "hosts", ",") AssertNoError(t, err) - AssertEqual(t, []string{"a", "b"}, ax7CollectStrings(seq)) + AssertEqual(t, []string{"a", "b"}, fixtureCollectStrings(seq)) } func TestScope_ScopedStore_GetSplit_Bad(t *T) { @@ -1917,18 +1925,18 @@ func TestScope_ScopedStore_GetSplit_Bad(t *T) { } func TestScope_ScopedStore_GetSplit_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) seq, err := scopedStore.GetSplit("config", "missing", ",") AssertErrorIs(t, err, NotFoundError) AssertNil(t, seq) } func TestScope_ScopedStore_GetFields_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetIn("config", "flags", "a b")) seq, err := scopedStore.GetFields("config", "flags") AssertNoError(t, err) - AssertEqual(t, []string{"a", "b"}, ax7CollectStrings(seq)) + AssertEqual(t, []string{"a", "b"}, fixtureCollectStrings(seq)) } func TestScope_ScopedStore_GetFields_Bad(t *T) { @@ -1939,14 +1947,14 @@ func TestScope_ScopedStore_GetFields_Bad(t *T) { } func TestScope_ScopedStore_GetFields_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) seq, err := scopedStore.GetFields("config", "missing") AssertErrorIs(t, err, NotFoundError) AssertNil(t, seq) } func TestScope_ScopedStore_PurgeExpired_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) RequireNoError(t, scopedStore.SetWithTTL("sessions", "token", "abc", -Millisecond)) removed, err := scopedStore.PurgeExpired() AssertNoError(t, err) @@ -1961,14 +1969,14 @@ func TestScope_ScopedStore_PurgeExpired_Bad(t *T) { } func TestScope_ScopedStore_PurgeExpired_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) removed, err := scopedStore.PurgeExpired() AssertNoError(t, err) AssertEqual(t, int64(0), removed) } func TestScope_ScopedStore_Watch_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) events := scopedStore.Watch("config") RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) event := <-events @@ -1983,7 +1991,7 @@ func TestScope_ScopedStore_Watch_Bad(t *T) { } func TestScope_ScopedStore_Watch_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) events := scopedStore.Watch("*") RequireNoError(t, scopedStore.SetIn("config", "colour", "blue")) event := <-events @@ -1991,7 +1999,7 @@ func TestScope_ScopedStore_Watch_Ugly(t *T) { } func TestScope_ScopedStore_Unwatch_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) events := scopedStore.Watch("config") scopedStore.Unwatch("config", events) _, ok := <-events @@ -2005,14 +2013,14 @@ func TestScope_ScopedStore_Unwatch_Bad(t *T) { } func TestScope_ScopedStore_Unwatch_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) events := scopedStore.Watch("config") scopedStore.Unwatch("config", events) AssertNotPanics(t, func() { scopedStore.Unwatch("config", events) }) } func TestScope_ScopedStore_OnChange_Good(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) called := false unregister := scopedStore.OnChange(func(event Event) { called = event.Group == "config" }) defer unregister() @@ -2030,7 +2038,7 @@ func TestScope_ScopedStore_OnChange_Bad(t *T) { } func TestScope_ScopedStore_OnChange_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) count := 0 unregister := scopedStore.OnChange(func(Event) { count++ }) unregister() @@ -2039,34 +2047,34 @@ func TestScope_ScopedStore_OnChange_Ugly(t *T) { } func TestScope_ScopedStore_Transaction_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { return transaction.Set("colour", "blue") }) + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { return transaction.Set("colour", "blue") }) AssertNoError(t, err) - AssertTrue(t, ax7ScopedExists(t, scopedStore, "colour")) + AssertTrue(t, fixtureScopedExists(t, scopedStore, "colour")) } func TestScope_ScopedStore_Transaction_Bad(t *T) { - scopedStore := ax7ScopedStore(t) + scopedStore := fixtureScopedStore(t) err := scopedStore.Transaction(nil) AssertError(t, err) - AssertFalse(t, ax7ScopedExists(t, scopedStore, "colour")) + AssertFalse(t, fixtureScopedExists(t, scopedStore, "colour")) } func TestScope_ScopedStore_Transaction_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { return NewError("rollback") }) + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { return core.Fail(NewError("rollback")) }) AssertError(t, err) - AssertFalse(t, ax7ScopedExists(t, scopedStore, "colour")) + AssertFalse(t, fixtureScopedExists(t, scopedStore, "colour")) } func TestScope_ScopedStoreTransaction_Exists_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.Set("colour", "blue")) exists, err := transaction.Exists("colour") AssertNoError(t, err) AssertTrue(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2079,24 +2087,24 @@ func TestScope_ScopedStoreTransaction_Exists_Bad(t *T) { } func TestScope_ScopedStoreTransaction_Exists_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { exists, err := transaction.Exists("missing") AssertNoError(t, err) AssertFalse(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_ExistsIn_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "colour", "blue")) exists, err := transaction.ExistsIn("config", "colour") AssertNoError(t, err) AssertTrue(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2109,24 +2117,24 @@ func TestScope_ScopedStoreTransaction_ExistsIn_Bad(t *T) { } func TestScope_ScopedStoreTransaction_ExistsIn_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { exists, err := transaction.ExistsIn("config", "missing") AssertNoError(t, err) AssertFalse(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_GroupExists_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "colour", "blue")) exists, err := transaction.GroupExists("config") AssertNoError(t, err) AssertTrue(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2139,24 +2147,24 @@ func TestScope_ScopedStoreTransaction_GroupExists_Bad(t *T) { } func TestScope_ScopedStoreTransaction_GroupExists_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { exists, err := transaction.GroupExists("missing") AssertNoError(t, err) AssertFalse(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_Get_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.Set("colour", "blue")) got, err := transaction.Get("colour") AssertNoError(t, err) AssertEqual(t, "blue", got) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2169,24 +2177,24 @@ func TestScope_ScopedStoreTransaction_Get_Bad(t *T) { } func TestScope_ScopedStoreTransaction_Get_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { got, err := transaction.Get("missing") AssertErrorIs(t, err, NotFoundError) AssertEqual(t, "", got) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_GetFrom_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "colour", "blue")) got, err := transaction.GetFrom("config", "colour") AssertNoError(t, err) AssertEqual(t, "blue", got) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2199,25 +2207,25 @@ func TestScope_ScopedStoreTransaction_GetFrom_Bad(t *T) { } func TestScope_ScopedStoreTransaction_GetFrom_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { got, err := transaction.GetFrom("config", "missing") AssertErrorIs(t, err, NotFoundError) AssertEqual(t, "", got) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_Set_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { err := transaction.Set("colour", "blue") AssertNoError(t, err) exists, err := transaction.Exists("colour") AssertNoError(t, err) AssertTrue(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2229,25 +2237,25 @@ func TestScope_ScopedStoreTransaction_Set_Bad(t *T) { } func TestScope_ScopedStoreTransaction_Set_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.Set("colour", "blue")) err := transaction.Set("shape", "circle") AssertNoError(t, err) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_SetIn_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { err := transaction.SetIn("config", "colour", "blue") AssertNoError(t, err) exists, err := transaction.ExistsIn("config", "colour") AssertNoError(t, err) AssertTrue(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2259,25 +2267,25 @@ func TestScope_ScopedStoreTransaction_SetIn_Bad(t *T) { } func TestScope_ScopedStoreTransaction_SetIn_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "colour", "blue")) err := transaction.SetIn("config", "colour", "green") AssertNoError(t, err) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_SetWithTTL_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { err := transaction.SetWithTTL("sessions", "token", "abc", Hour) AssertNoError(t, err) exists, err := transaction.ExistsIn("sessions", "token") AssertNoError(t, err) AssertTrue(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2289,27 +2297,27 @@ func TestScope_ScopedStoreTransaction_SetWithTTL_Bad(t *T) { } func TestScope_ScopedStoreTransaction_SetWithTTL_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetWithTTL("sessions", "token", "abc", -Millisecond)) exists, err := transaction.ExistsIn("sessions", "token") AssertNoError(t, err) AssertFalse(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_Delete_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "colour", "blue")) err := transaction.Delete("config", "colour") AssertNoError(t, err) exists, err := transaction.ExistsIn("config", "colour") AssertNoError(t, err) AssertFalse(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2321,28 +2329,28 @@ func TestScope_ScopedStoreTransaction_Delete_Bad(t *T) { } func TestScope_ScopedStoreTransaction_Delete_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { err := transaction.Delete("missing", "key") AssertNoError(t, err) exists, err := transaction.ExistsIn("missing", "key") AssertNoError(t, err) AssertFalse(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_DeleteGroup_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "colour", "blue")) err := transaction.DeleteGroup("config") AssertNoError(t, err) exists, err := transaction.GroupExists("config") AssertNoError(t, err) AssertFalse(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2354,28 +2362,28 @@ func TestScope_ScopedStoreTransaction_DeleteGroup_Bad(t *T) { } func TestScope_ScopedStoreTransaction_DeleteGroup_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { err := transaction.DeleteGroup("missing") AssertNoError(t, err) exists, err := transaction.GroupExists("missing") AssertNoError(t, err) AssertFalse(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_DeletePrefix_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn(testCacheA, "colour", "blue")) err := transaction.DeletePrefix("cache") AssertNoError(t, err) exists, err := transaction.GroupExists(testCacheA) AssertNoError(t, err) AssertFalse(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2387,26 +2395,26 @@ func TestScope_ScopedStoreTransaction_DeletePrefix_Bad(t *T) { } func TestScope_ScopedStoreTransaction_DeletePrefix_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { err := transaction.DeletePrefix("missing") AssertNoError(t, err) groups, err := transaction.Groups("missing") AssertNoError(t, err) AssertEmpty(t, groups) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_GetAll_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "colour", "blue")) entries, err := transaction.GetAll("config") AssertNoError(t, err) AssertEqual(t, "blue", entries["colour"]) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2419,24 +2427,24 @@ func TestScope_ScopedStoreTransaction_GetAll_Bad(t *T) { } func TestScope_ScopedStoreTransaction_GetAll_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { entries, err := transaction.GetAll("missing") AssertNoError(t, err) AssertEmpty(t, entries) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_GetPage_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "colour", "blue")) page, err := transaction.GetPage("config", 0, 1) AssertNoError(t, err) AssertEqual(t, "colour", page[0].Key) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2449,84 +2457,84 @@ func TestScope_ScopedStoreTransaction_GetPage_Bad(t *T) { } func TestScope_ScopedStoreTransaction_GetPage_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { page, err := transaction.GetPage("missing", 0, 1) AssertNoError(t, err) AssertEmpty(t, page) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_All_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "colour", "blue")) - entries, err := ax7CollectKeyValues(transaction.All("config")) + entries, err := fixtureCollectKeyValues(transaction.All("config")) AssertNoError(t, err) AssertEqual(t, "colour", entries[0].Key) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_All_Bad(t *T) { var transaction *ScopedStoreTransaction - entries, err := ax7CollectKeyValues(transaction.All("config")) + entries, err := fixtureCollectKeyValues(transaction.All("config")) AssertError(t, err) AssertEmpty(t, entries) } func TestScope_ScopedStoreTransaction_All_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { - entries, err := ax7CollectKeyValues(transaction.All("missing")) + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + entries, err := fixtureCollectKeyValues(transaction.All("missing")) AssertNoError(t, err) AssertEmpty(t, entries) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_AllSeq_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "colour", "blue")) - entries, err := ax7CollectKeyValues(transaction.AllSeq("config")) + entries, err := fixtureCollectKeyValues(transaction.AllSeq("config")) AssertNoError(t, err) AssertEqual(t, "blue", entries[0].Value) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_AllSeq_Bad(t *T) { var transaction *ScopedStoreTransaction - entries, err := ax7CollectKeyValues(transaction.AllSeq("config")) + entries, err := fixtureCollectKeyValues(transaction.AllSeq("config")) AssertError(t, err) AssertEmpty(t, entries) } func TestScope_ScopedStoreTransaction_AllSeq_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { - entries, err := ax7CollectKeyValues(transaction.AllSeq("missing")) + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + entries, err := fixtureCollectKeyValues(transaction.AllSeq("missing")) AssertNoError(t, err) AssertEmpty(t, entries) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_Count_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "colour", "blue")) count, err := transaction.Count("config") AssertNoError(t, err) AssertEqual(t, 1, count) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2539,24 +2547,24 @@ func TestScope_ScopedStoreTransaction_Count_Bad(t *T) { } func TestScope_ScopedStoreTransaction_Count_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { count, err := transaction.Count("missing") AssertNoError(t, err) AssertEqual(t, 0, count) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_CountAll_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "colour", "blue")) count, err := transaction.CountAll() AssertNoError(t, err) AssertEqual(t, 1, count) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2569,24 +2577,24 @@ func TestScope_ScopedStoreTransaction_CountAll_Bad(t *T) { } func TestScope_ScopedStoreTransaction_CountAll_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { count, err := transaction.CountAll("missing") AssertNoError(t, err) AssertEqual(t, 0, count) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_Groups_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "colour", "blue")) groups, err := transaction.Groups() AssertNoError(t, err) AssertEqual(t, []string{"config"}, groups) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2599,54 +2607,54 @@ func TestScope_ScopedStoreTransaction_Groups_Bad(t *T) { } func TestScope_ScopedStoreTransaction_Groups_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { groups, err := transaction.Groups("missing") AssertNoError(t, err) AssertEmpty(t, groups) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_GroupsSeq_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "colour", "blue")) - groups, err := ax7CollectGroups(transaction.GroupsSeq()) + groups, err := fixtureCollectGroups(transaction.GroupsSeq()) AssertNoError(t, err) AssertEqual(t, []string{"config"}, groups) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_GroupsSeq_Bad(t *T) { var transaction *ScopedStoreTransaction - groups, err := ax7CollectGroups(transaction.GroupsSeq()) + groups, err := fixtureCollectGroups(transaction.GroupsSeq()) AssertError(t, err) AssertEmpty(t, groups) } func TestScope_ScopedStoreTransaction_GroupsSeq_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { - groups, err := ax7CollectGroups(transaction.GroupsSeq("missing")) + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + groups, err := fixtureCollectGroups(transaction.GroupsSeq("missing")) AssertNoError(t, err) AssertEmpty(t, groups) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_Render_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "name", "alice")) rendered, err := transaction.Render("hello {{ .name }}", "config") AssertNoError(t, err) AssertEqual(t, "hello alice", rendered) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2659,24 +2667,24 @@ func TestScope_ScopedStoreTransaction_Render_Bad(t *T) { } func TestScope_ScopedStoreTransaction_Render_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { rendered, err := transaction.Render("empty", "missing") AssertNoError(t, err) AssertEqual(t, "empty", rendered) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_GetSplit_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "hosts", "a,b")) seq, err := transaction.GetSplit("config", "hosts", ",") AssertNoError(t, err) - AssertEqual(t, []string{"a", "b"}, ax7CollectStrings(seq)) - return nil + AssertEqual(t, []string{"a", "b"}, fixtureCollectStrings(seq)) + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2689,24 +2697,24 @@ func TestScope_ScopedStoreTransaction_GetSplit_Bad(t *T) { } func TestScope_ScopedStoreTransaction_GetSplit_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { seq, err := transaction.GetSplit("missing", "hosts", ",") AssertErrorIs(t, err, NotFoundError) AssertNil(t, seq) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_GetFields_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetIn("config", "flags", "a b")) seq, err := transaction.GetFields("config", "flags") AssertNoError(t, err) - AssertEqual(t, []string{"a", "b"}, ax7CollectStrings(seq)) - return nil + AssertEqual(t, []string{"a", "b"}, fixtureCollectStrings(seq)) + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2719,24 +2727,24 @@ func TestScope_ScopedStoreTransaction_GetFields_Bad(t *T) { } func TestScope_ScopedStoreTransaction_GetFields_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { seq, err := transaction.GetFields("missing", "flags") AssertErrorIs(t, err, NotFoundError) AssertNil(t, seq) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestScope_ScopedStoreTransaction_PurgeExpired_Good(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { RequireNoError(t, transaction.SetWithTTL("sessions", "token", "abc", -Millisecond)) removed, err := transaction.PurgeExpired() AssertNoError(t, err) AssertEqual(t, int64(1), removed) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -2749,12 +2757,12 @@ func TestScope_ScopedStoreTransaction_PurgeExpired_Bad(t *T) { } func TestScope_ScopedStoreTransaction_PurgeExpired_Ugly(t *T) { - scopedStore := ax7ScopedStore(t) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + scopedStore := fixtureScopedStore(t) + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { removed, err := transaction.PurgeExpired() AssertNoError(t, err) AssertEqual(t, int64(0), removed) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } diff --git a/store.go b/go/store.go similarity index 76% rename from store.go rename to go/store.go index fd046ab..7946383 100644 --- a/store.go +++ b/go/store.go @@ -136,23 +136,24 @@ func (storeConfig StoreConfig) Normalised() StoreConfig { } // Usage example: `if err := (store.StoreConfig{DatabasePath: ":memory:", PurgeInterval: 30 * time.Second}).Validate(); err != nil { return }` -func (storeConfig StoreConfig) Validate() error { +func (storeConfig StoreConfig) Validate() core.Result { if storeConfig.DatabasePath == "" { - return core.E( + return core.Fail(core.E( opStoreConfigValidate, "database path is empty", nil, - ) + )) } if storeConfig.Journal != (JournalConfiguration{}) { - if err := storeConfig.Journal.Validate(); err != nil { - return core.E(opStoreConfigValidate, "journal config", err) + if result := storeConfig.Journal.Validate(); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opStoreConfigValidate, "journal config", err)) } } if storeConfig.PurgeInterval < 0 { - return core.E(opStoreConfigValidate, "purge interval must be zero or positive", nil) + return core.Fail(core.E(opStoreConfigValidate, "purge interval must be zero or positive", nil)) } - return nil + return core.Ok(nil) } // Usage example: `config := store.JournalConfiguration{EndpointURL: "http://127.0.0.1:8086", Organisation: "core", BucketName: "events"}` @@ -169,28 +170,28 @@ type JournalConfiguration struct { } // Usage example: `if err := (store.JournalConfiguration{EndpointURL: "http://127.0.0.1:8086", Organisation: "core", BucketName: "events"}).Validate(); err != nil { return }` -func (journalConfig JournalConfiguration) Validate() error { +func (journalConfig JournalConfiguration) Validate() core.Result { switch { case journalConfig.EndpointURL == "": - return core.E( + return core.Fail(core.E( opJournalConfigurationValidate, `endpoint URL is empty; use values like "http://127.0.0.1:8086"`, nil, - ) + )) case journalConfig.Organisation == "": - return core.E( + return core.Fail(core.E( opJournalConfigurationValidate, `organisation is empty; use values like "core"`, nil, - ) + )) case journalConfig.BucketName == "": - return core.E( + return core.Fail(core.E( opJournalConfigurationValidate, `bucket name is empty; use values like "events"`, nil, - ) + )) default: - return nil + return core.Ok(nil) } } @@ -238,9 +239,9 @@ type Store struct { cachedOrphanWorkspaces []*Workspace } -func (storeInstance *Store) ensureReady(operation string) error { +func (storeInstance *Store) ensureReady(operation string) core.Result { if storeInstance == nil { - return core.E(operation, "store is nil", nil) + return core.Fail(core.E(operation, "store is nil", nil)) } if storeInstance.db == nil { storeInstance.db = storeInstance.sqliteDatabase @@ -249,17 +250,17 @@ func (storeInstance *Store) ensureReady(operation string) error { storeInstance.sqliteDatabase = storeInstance.db } if storeInstance.db == nil || storeInstance.sqliteDatabase == nil { - return core.E(operation, "store is not initialised", nil) + return core.Fail(core.E(operation, "store is not initialised", nil)) } storeInstance.lifecycleLock.Lock() closed := storeInstance.isClosed || storeInstance.isClosing storeInstance.lifecycleLock.Unlock() if closed { - return core.E(operation, "store is closed", nil) + return core.Fail(core.E(operation, "store is closed", nil)) } - return nil + return core.Ok(nil) } // Usage example: `storeInstance, err := store.NewConfigured(store.StoreConfig{DatabasePath: "/tmp/go-store.db", Journal: store.JournalConfiguration{EndpointURL: "http://127.0.0.1:8086", Organisation: "core", BucketName: "events"}})` @@ -363,19 +364,20 @@ func WithPurgeInterval(interval time.Duration) StoreOption { } // Usage example: `storeInstance, err := store.NewConfigured(store.StoreConfig{DatabasePath: ":memory:", Journal: store.JournalConfiguration{EndpointURL: "http://127.0.0.1:8086", Organisation: "core", BucketName: "events"}, PurgeInterval: 20 * time.Millisecond})` -func NewConfigured(storeConfig StoreConfig) (*Store, error) { +func NewConfigured(storeConfig StoreConfig) (*Store, core.Result) { return openConfiguredStore("store.NewConfigured", storeConfig) } -func openConfiguredStore(operation string, storeConfig StoreConfig) (*Store, error) { - if err := storeConfig.Validate(); err != nil { - return nil, core.E(operation, "validate config", err) +func openConfiguredStore(operation string, storeConfig StoreConfig) (*Store, core.Result) { + if result := storeConfig.Validate(); !result.OK { + err, _ := result.Value.(error) + return nil, core.Fail(core.E(operation, "validate config", err)) } storeConfig = storeConfig.Normalised() - storeInstance, err := openSQLiteStore(operation, storeConfig.DatabasePath, storeConfig.Medium) - if err != nil { - return nil, err + storeInstance, result := openSQLiteStore(operation, storeConfig.DatabasePath, storeConfig.Medium) + if !result.OK { + return nil, result } if storeConfig.Journal != (JournalConfiguration{}) { @@ -392,11 +394,11 @@ func openConfiguredStore(operation string, storeConfig StoreConfig) (*Store, err // leftover workspaces via RecoverOrphans(). storeInstance.cachedOrphanWorkspaces = discoverOrphanWorkspaces(storeInstance.workspaceStateDirectoryPath(), storeInstance) storeInstance.startBackgroundPurge() - return storeInstance, nil + return storeInstance, core.Ok(nil) } // Usage example: `storeInstance, err := store.NewConfigured(store.StoreConfig{DatabasePath: "/tmp/go-store.db", Journal: store.JournalConfiguration{EndpointURL: "http://127.0.0.1:8086", Organisation: "core", BucketName: "events"}})` -func New(databasePath string, options ...StoreOption) (*Store, error) { +func New(databasePath string, options ...StoreOption) (*Store, core.Result) { scratch := &Store{ databasePath: databasePath, workspaceStateDirectory: normaliseWorkspaceStateDirectory(defaultWorkspaceStateDirectory), @@ -416,26 +418,28 @@ func New(databasePath string, options ...StoreOption) (*Store, error) { storeConfig.WorkspaceStateDirectory = scratch.WorkspaceStateDirectory() storeConfig.Medium = scratch.medium - storeInstance, err := openConfiguredStore(opNew, storeConfig) - if err != nil { - return nil, err + storeInstance, result := openConfiguredStore(opNew, storeConfig) + if !result.OK { + return nil, result } - return storeInstance, nil + return storeInstance, core.Ok(nil) } -func openSQLiteStore(operation, databasePath string, medium Medium) (*Store, error) { - storage, err := prepareSQLiteStorage(operation, databasePath, medium) - if err != nil { - return nil, err +func openSQLiteStore(operation, databasePath string, medium Medium) (*Store, core.Result) { + storage, result := prepareSQLiteStorage(operation, databasePath, medium) + if !result.OK { + return nil, result } - sqliteDatabase, err := sql.Open("sqlite", storage.path) - if err != nil { - return nil, core.E(operation, "open database", err) + sqliteDatabase, openErr := sql.Open("sqlite", storage.path) + if openErr != nil { + return nil, core.Fail(core.E(operation, "open database", openErr)) } - if err := configureSQLiteDatabase(operation, sqliteDatabase); err != nil { - _ = sqliteDatabase.Close() - return nil, err + if result := configureSQLiteDatabase(operation, sqliteDatabase); !result.OK { + if closeErr := sqliteDatabase.Close(); closeErr != nil { + core.Error("sqlite close after configure failed", "err", closeErr) + } + return nil, result } purgeContext, cancel := context.WithCancel(context.Background()) @@ -452,7 +456,7 @@ func openSQLiteStore(operation, databasePath string, medium Medium) (*Store, err mediumBacked: storage.mediumBacked, medium: medium, watchers: make(map[string][]chan Event), - }, nil + }, core.Ok(nil) } type sqliteStorageConfig struct { @@ -461,44 +465,46 @@ type sqliteStorageConfig struct { mediumBacked bool } -func prepareSQLiteStorage(operation, databasePath string, medium Medium) (sqliteStorageConfig, error) { +func prepareSQLiteStorage(operation, databasePath string, medium Medium) (sqliteStorageConfig, core.Result) { storage := sqliteStorageConfig{path: databasePath} storage.mediumBacked = medium != nil && databasePath != "" && databasePath != memoryDatabasePath if !storage.mediumBacked { - return storage, nil + return storage, core.Ok(nil) } filesystem := (&core.Fs{}).NewUnrestricted() storage.directory = filesystem.TempDir("go-store") storage.path = core.Path(storage.directory, "store.db") if !medium.Exists(databasePath) { - return storage, nil + return storage, core.Ok(nil) } - content, err := medium.Read(databasePath) - if err != nil { - return sqliteStorageConfig{}, core.E(operation, "read database from medium", err) + content, result := medium.Read(databasePath) + if !result.OK { + err, _ := result.Value.(error) + return sqliteStorageConfig{}, core.Fail(core.E(operation, "read database from medium", err)) } if result := filesystem.Write(storage.path, content); !result.OK { - return sqliteStorageConfig{}, core.E(operation, "seed sqlite file from medium", result.Value.(error)) + return sqliteStorageConfig{}, core.Fail(core.E(operation, "seed sqlite file from medium", result.Value.(error))) } - return storage, nil + return storage, core.Ok(nil) } -func configureSQLiteDatabase(operation string, sqliteDatabase *sql.DB) error { +func configureSQLiteDatabase(operation string, sqliteDatabase *sql.DB) core.Result { // Serialise all access through a single connection. SQLite only supports // one writer at a time; using a pool causes SQLITE_BUSY under contention // because pragmas (journal_mode, busy_timeout) are per-connection and the // pool hands out different connections for each call. sqliteDatabase.SetMaxOpenConns(1) if _, err := sqliteDatabase.Exec(sqliteWALPragma); err != nil { - return core.E(operation, "set WAL journal mode", err) + return core.Fail(core.E(operation, "set WAL journal mode", err)) } if _, err := sqliteDatabase.Exec("PRAGMA busy_timeout=5000"); err != nil { - return core.E(operation, "set busy timeout", err) + return core.Fail(core.E(operation, "set busy timeout", err)) } - if err := ensureSchema(sqliteDatabase); err != nil { - return core.E(operation, "ensure schema", err) + if result := ensureSchema(sqliteDatabase); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(operation, "ensure schema", err)) } - return nil + return core.Ok(nil) } func (storeInstance *Store) workspaceStateDirectoryPath() string { @@ -509,9 +515,9 @@ func (storeInstance *Store) workspaceStateDirectoryPath() string { } // Usage example: `storeInstance, err := store.New(":memory:"); if err != nil { return }; defer func() { _ = storeInstance.Close() }()` -func (storeInstance *Store) Close() error { +func (storeInstance *Store) Close() core.Result { if storeInstance == nil { - return nil + return core.Ok(nil) } storeInstance.closeLock.Lock() @@ -520,7 +526,7 @@ func (storeInstance *Store) Close() error { storeInstance.lifecycleLock.Lock() if storeInstance.isClosed { storeInstance.lifecycleLock.Unlock() - return nil + return core.Ok(nil) } storeInstance.isClosing = true storeInstance.lifecycleLock.Unlock() @@ -548,10 +554,10 @@ func (storeInstance *Store) Close() error { storeInstance.callbackLock.Unlock() storeInstance.orphanWorkspaceLock.Lock() - var orphanCleanupErr error + var orphanCleanupResult core.Result for _, orphanWorkspace := range storeInstance.cachedOrphanWorkspaces { - if err := orphanWorkspace.closeWithoutRemovingFiles(); err != nil && orphanCleanupErr == nil { - orphanCleanupErr = err + if result := orphanWorkspace.closeWithoutRemovingFiles(); !result.OK && orphanCleanupResult.Value == nil { + orphanCleanupResult = result } } storeInstance.cachedOrphanWorkspaces = nil @@ -565,19 +571,24 @@ func (storeInstance *Store) Close() error { } if storeInstance.sqliteDatabase == nil { storeInstance.markClosed() - return orphanCleanupErr + if orphanCleanupResult.Value != nil { + return orphanCleanupResult + } + return core.Ok(nil) } if err := storeInstance.sqliteDatabase.Close(); err != nil { - return core.E(opClose, "database close", err) + return core.Fail(core.E(opClose, "database close", err)) } - if err := storeInstance.syncMediumBackedDatabase(); err != nil { - return core.E(opClose, "sync medium-backed database", err) + if result := storeInstance.syncMediumBackedDatabase(); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opClose, "sync medium-backed database", err)) } storeInstance.markClosed() - if orphanCleanupErr != nil { - return core.E(opClose, "close orphan workspaces", orphanCleanupErr) + if orphanCleanupResult.Value != nil { + err, _ := orphanCleanupResult.Value.(error) + return core.Fail(core.E(opClose, "close orphan workspaces", err)) } - return orphanCleanupErr + return core.Ok(nil) } func (storeInstance *Store) markClosed() { @@ -587,40 +598,44 @@ func (storeInstance *Store) markClosed() { storeInstance.lifecycleLock.Unlock() } -func (storeInstance *Store) syncMediumBackedDatabase() error { +func (storeInstance *Store) syncMediumBackedDatabase() core.Result { if storeInstance == nil || !storeInstance.mediumBacked || storeInstance.medium == nil { - return nil + return core.Ok(nil) } if storeInstance.databasePath == "" || storeInstance.databasePath == memoryDatabasePath { - return nil + return core.Ok(nil) } if storeInstance.sqliteStoragePath == "" { - return nil + return core.Ok(nil) } filesystem := (&core.Fs{}).NewUnrestricted() readResult := filesystem.Read(storeInstance.sqliteStoragePath) if !readResult.OK { - return readResult.Value.(error) + return readResult } - if err := storeInstance.medium.Write(storeInstance.databasePath, readResult.Value.(string)); err != nil { - return err + if result := storeInstance.medium.Write(storeInstance.databasePath, readResult.Value.(string)); !result.OK { + return result } if storeInstance.sqliteStorageDirectory != "" { - _ = filesystem.DeleteAll(storeInstance.sqliteStorageDirectory) - return nil + if result := filesystem.DeleteAll(storeInstance.sqliteStorageDirectory); !result.OK { + core.Error("sqlite storage directory cleanup failed", "err", result.Error()) + } + return core.Ok(nil) } for _, path := range []string{storeInstance.sqliteStoragePath + "-wal", storeInstance.sqliteStoragePath + "-shm"} { - _ = filesystem.Delete(path) + if result := filesystem.Delete(path); !result.OK { + core.Error("sqlite sidecar cleanup failed", "file", path, "err", result.Error()) + } } - return nil + return core.Ok(nil) } // Usage example: `colourValue, err := storeInstance.Get("config", "colour")` -func (storeInstance *Store) Get(group, key string) (string, error) { - if err := storeInstance.ensureReady(opGet); err != nil { - return "", err +func (storeInstance *Store) Get(group, key string) (string, core.Result) { + if result := storeInstance.ensureReady(opGet); !result.OK { + return "", result } var value string @@ -630,24 +645,25 @@ func (storeInstance *Store) Get(group, key string) (string, error) { group, key, ).Scan(&value, &expiresAt) if err == sql.ErrNoRows { - return "", core.E(opGet, core.Concat(group, "/", key), NotFoundError) + return "", core.Fail(core.E(opGet, core.Concat(group, "/", key), NotFoundError)) } if err != nil { - return "", core.E(opGet, "query row", err) + return "", core.Fail(core.E(opGet, "query row", err)) } if expiresAt.Valid && expiresAt.Int64 <= time.Now().UnixMilli() { - if err := storeInstance.Delete(group, key); err != nil { - return "", core.E(opGet, "delete expired row", err) + if result := storeInstance.Delete(group, key); !result.OK { + err, _ := result.Value.(error) + return "", core.Fail(core.E(opGet, "delete expired row", err)) } - return "", core.E(opGet, core.Concat(group, "/", key), NotFoundError) + return "", core.Fail(core.E(opGet, core.Concat(group, "/", key), NotFoundError)) } - return value, nil + return value, core.Ok(nil) } // Usage example: `if err := storeInstance.Set("config", "colour", "blue"); err != nil { return }` -func (storeInstance *Store) Set(group, key, value string) error { - if err := storeInstance.ensureReady("store.Set"); err != nil { - return err +func (storeInstance *Store) Set(group, key, value string) core.Result { + if result := storeInstance.ensureReady("store.Set"); !result.OK { + return result } _, err := storeInstance.sqliteDatabase.Exec( @@ -656,16 +672,16 @@ func (storeInstance *Store) Set(group, key, value string) error { group, key, value, ) if err != nil { - return core.E("store.Set", "execute upsert", err) + return core.Fail(core.E("store.Set", "execute upsert", err)) } storeInstance.notify(Event{Type: EventSet, Group: group, Key: key, Value: value, Timestamp: time.Now()}) - return nil + return core.Ok(nil) } // Usage example: `if err := storeInstance.SetWithTTL("session", "token", "abc123", time.Minute); err != nil { return }` -func (storeInstance *Store) SetWithTTL(group, key, value string, timeToLive time.Duration) error { - if err := storeInstance.ensureReady("store.SetWithTTL"); err != nil { - return err +func (storeInstance *Store) SetWithTTL(group, key, value string, timeToLive time.Duration) core.Result { + if result := storeInstance.ensureReady("store.SetWithTTL"); !result.OK { + return result } expiresAt := time.Now().Add(timeToLive).UnixMilli() @@ -675,37 +691,37 @@ func (storeInstance *Store) SetWithTTL(group, key, value string, timeToLive time group, key, value, expiresAt, ) if err != nil { - return core.E("store.SetWithTTL", "execute upsert with expiry", err) + return core.Fail(core.E("store.SetWithTTL", "execute upsert with expiry", err)) } storeInstance.notify(Event{Type: EventSet, Group: group, Key: key, Value: value, Timestamp: time.Now()}) - return nil + return core.Ok(nil) } // Usage example: `if err := storeInstance.Delete("config", "colour"); err != nil { return }` -func (storeInstance *Store) Delete(group, key string) error { - if err := storeInstance.ensureReady(opDelete); err != nil { - return err +func (storeInstance *Store) Delete(group, key string) core.Result { + if result := storeInstance.ensureReady(opDelete); !result.OK { + return result } deleteResult, err := storeInstance.sqliteDatabase.Exec(sqlDeleteFrom+entriesTableName+sqlWhere+entryGroupColumn+" = ? AND "+entryKeyColumn+" = ?", group, key) if err != nil { - return core.E(opDelete, "delete row", err) + return core.Fail(core.E(opDelete, "delete row", err)) } deletedRows, rowsAffectedError := deleteResult.RowsAffected() if rowsAffectedError != nil { - return core.E(opDelete, "count deleted rows", rowsAffectedError) + return core.Fail(core.E(opDelete, "count deleted rows", rowsAffectedError)) } if deletedRows > 0 { storeInstance.notify(Event{Type: EventDelete, Group: group, Key: key, Timestamp: time.Now()}) } - return nil + return core.Ok(nil) } // Usage example: `exists, err := storeInstance.Exists("config", "colour")` // Usage example: `if exists, _ := storeInstance.Exists("session", "token"); !exists { fmt.Println("session expired") }` -func (storeInstance *Store) Exists(group, key string) (bool, error) { - if err := storeInstance.ensureReady("store.Exists"); err != nil { - return false, err +func (storeInstance *Store) Exists(group, key string) (bool, core.Result) { + if result := storeInstance.ensureReady("store.Exists"); !result.OK { + return false, result } return liveEntryExists(storeInstance.sqliteDatabase, group, key) @@ -713,22 +729,22 @@ func (storeInstance *Store) Exists(group, key string) (bool, error) { // Usage example: `exists, err := storeInstance.GroupExists("config")` // Usage example: `if exists, _ := storeInstance.GroupExists("tenant-a:config"); !exists { fmt.Println("group is empty") }` -func (storeInstance *Store) GroupExists(group string) (bool, error) { - if err := storeInstance.ensureReady("store.GroupExists"); err != nil { - return false, err +func (storeInstance *Store) GroupExists(group string) (bool, core.Result) { + if result := storeInstance.ensureReady("store.GroupExists"); !result.OK { + return false, result } - count, err := storeInstance.Count(group) - if err != nil { - return false, err + count, result := storeInstance.Count(group) + if !result.OK { + return false, result } - return count > 0, nil + return count > 0, core.Ok(nil) } // Usage example: `keyCount, err := storeInstance.Count("config")` -func (storeInstance *Store) Count(group string) (int, error) { - if err := storeInstance.ensureReady("store.Count"); err != nil { - return 0, err +func (storeInstance *Store) Count(group string) (int, core.Result) { + if result := storeInstance.ensureReady("store.Count"); !result.OK { + return 0, result } var count int @@ -737,35 +753,35 @@ func (storeInstance *Store) Count(group string) (int, error) { group, time.Now().UnixMilli(), ).Scan(&count) if err != nil { - return 0, core.E("store.Count", "count rows", err) + return 0, core.Fail(core.E("store.Count", "count rows", err)) } - return count, nil + return count, core.Ok(nil) } // Usage example: `if err := storeInstance.DeleteGroup("cache"); err != nil { return }` -func (storeInstance *Store) DeleteGroup(group string) error { - if err := storeInstance.ensureReady(opDeleteGroup); err != nil { - return err +func (storeInstance *Store) DeleteGroup(group string) core.Result { + if result := storeInstance.ensureReady(opDeleteGroup); !result.OK { + return result } deleteResult, err := storeInstance.sqliteDatabase.Exec(sqlDeleteFrom+entriesTableName+sqlWhere+entryGroupColumn+" = ?", group) if err != nil { - return core.E(opDeleteGroup, "delete group", err) + return core.Fail(core.E(opDeleteGroup, "delete group", err)) } deletedRows, rowsAffectedError := deleteResult.RowsAffected() if rowsAffectedError != nil { - return core.E(opDeleteGroup, "count deleted rows", rowsAffectedError) + return core.Fail(core.E(opDeleteGroup, "count deleted rows", rowsAffectedError)) } if deletedRows > 0 { storeInstance.notify(Event{Type: EventDeleteGroup, Group: group, Timestamp: time.Now()}) } - return nil + return core.Ok(nil) } // Usage example: `if err := storeInstance.DeletePrefix("tenant-a:"); err != nil { return }` -func (storeInstance *Store) DeletePrefix(groupPrefix string) error { - if err := storeInstance.ensureReady(opDeletePrefix); err != nil { - return err +func (storeInstance *Store) DeletePrefix(groupPrefix string) core.Result { + if result := storeInstance.ensureReady(opDeletePrefix); !result.OK { + return result } var rows *sql.Rows @@ -781,7 +797,7 @@ func (storeInstance *Store) DeletePrefix(groupPrefix string) error { ) } if err != nil { - return core.E(opDeletePrefix, "list groups", err) + return core.Fail(core.E(opDeletePrefix, "list groups", err)) } defer func() { _ = rows.Close() }() @@ -789,19 +805,20 @@ func (storeInstance *Store) DeletePrefix(groupPrefix string) error { for rows.Next() { var groupName string if err := rows.Scan(&groupName); err != nil { - return core.E(opDeletePrefix, "scan group name", err) + return core.Fail(core.E(opDeletePrefix, "scan group name", err)) } groupNames = append(groupNames, groupName) } if err := rows.Err(); err != nil { - return core.E(opDeletePrefix, "iterate groups", err) + return core.Fail(core.E(opDeletePrefix, "iterate groups", err)) } for _, groupName := range groupNames { - if err := storeInstance.DeleteGroup(groupName); err != nil { - return core.E(opDeletePrefix, "delete group", err) + if result := storeInstance.DeleteGroup(groupName); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opDeletePrefix, "delete group", err)) } } - return nil + return core.Ok(nil) } // Usage example: `for entry, err := range storeInstance.All("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }` @@ -813,31 +830,31 @@ type KeyValue struct { } // Usage example: `colourEntries, err := storeInstance.GetAll("config")` -func (storeInstance *Store) GetAll(group string) (map[string]string, error) { - if err := storeInstance.ensureReady("store.GetAll"); err != nil { - return nil, err +func (storeInstance *Store) GetAll(group string) (map[string]string, core.Result) { + if result := storeInstance.ensureReady("store.GetAll"); !result.OK { + return nil, result } entriesByKey := make(map[string]string) for entry, err := range storeInstance.All(group) { if err != nil { - return nil, core.E("store.GetAll", "iterate rows", err) + return nil, core.Fail(core.E("store.GetAll", "iterate rows", err)) } entriesByKey[entry.Key] = entry.Value } - return entriesByKey, nil + return entriesByKey, core.Ok(nil) } // Usage example: `page, err := storeInstance.GetPage("config", 0, 25); if err != nil { return }; for _, entry := range page { fmt.Println(entry.Key, entry.Value) }` -func (storeInstance *Store) GetPage(group string, offset, limit int) ([]KeyValue, error) { - if err := storeInstance.ensureReady(opGetPage); err != nil { - return nil, err +func (storeInstance *Store) GetPage(group string, offset, limit int) ([]KeyValue, core.Result) { + if result := storeInstance.ensureReady(opGetPage); !result.OK { + return nil, result } if offset < 0 { - return nil, core.E(opGetPage, "offset must be zero or positive", nil) + return nil, core.Fail(core.E(opGetPage, "offset must be zero or positive", nil)) } if limit < 0 { - return nil, core.E(opGetPage, "limit must be zero or positive", nil) + return nil, core.Fail(core.E(opGetPage, "limit must be zero or positive", nil)) } rows, err := storeInstance.sqliteDatabase.Query( @@ -845,7 +862,7 @@ func (storeInstance *Store) GetPage(group string, offset, limit int) ([]KeyValue group, time.Now().UnixMilli(), limit, offset, ) if err != nil { - return nil, core.E(opGetPage, "query rows", err) + return nil, core.Fail(core.E(opGetPage, "query rows", err)) } defer func() { _ = rows.Close() }() @@ -853,20 +870,21 @@ func (storeInstance *Store) GetPage(group string, offset, limit int) ([]KeyValue for rows.Next() { var entry KeyValue if err := rows.Scan(&entry.Key, &entry.Value); err != nil { - return nil, core.E(opGetPage, "scan row", err) + return nil, core.Fail(core.E(opGetPage, "scan row", err)) } page = append(page, entry) } if err := rows.Err(); err != nil { - return nil, core.E(opGetPage, rowsIterationMessage, err) + return nil, core.Fail(core.E(opGetPage, rowsIterationMessage, err)) } - return page, nil + return page, core.Ok(nil) } // Usage example: `for entry, err := range storeInstance.AllSeq("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }` func (storeInstance *Store) AllSeq(group string) iter.Seq2[KeyValue, error] { return func(yield func(KeyValue, error) bool) { - if err := storeInstance.ensureReady(opAll); err != nil { + if result := storeInstance.ensureReady(opAll); !result.OK { + err, _ := result.Value.(error) yield(KeyValue{}, err) return } @@ -909,60 +927,60 @@ func (storeInstance *Store) All(group string) iter.Seq2[KeyValue, error] { } // Usage example: `parts, err := storeInstance.GetSplit("config", "hosts", ","); if err != nil { return }; for part := range parts { fmt.Println(part) }` -func (storeInstance *Store) GetSplit(group, key, separator string) (iter.Seq[string], error) { - if err := storeInstance.ensureReady("store.GetSplit"); err != nil { - return nil, err +func (storeInstance *Store) GetSplit(group, key, separator string) (iter.Seq[string], core.Result) { + if result := storeInstance.ensureReady("store.GetSplit"); !result.OK { + return nil, result } - value, err := storeInstance.Get(group, key) - if err != nil { - return nil, err + value, result := storeInstance.Get(group, key) + if !result.OK { + return nil, result } - return splitValueSeq(value, separator), nil + return splitValueSeq(value, separator), core.Ok(nil) } // Usage example: `fields, err := storeInstance.GetFields("config", "flags"); if err != nil { return }; for field := range fields { fmt.Println(field) }` -func (storeInstance *Store) GetFields(group, key string) (iter.Seq[string], error) { - if err := storeInstance.ensureReady("store.GetFields"); err != nil { - return nil, err +func (storeInstance *Store) GetFields(group, key string) (iter.Seq[string], core.Result) { + if result := storeInstance.ensureReady("store.GetFields"); !result.OK { + return nil, result } - value, err := storeInstance.Get(group, key) - if err != nil { - return nil, err + value, result := storeInstance.Get(group, key) + if !result.OK { + return nil, result } - return fieldsValueSeq(value), nil + return fieldsValueSeq(value), core.Ok(nil) } // Usage example: `renderedTemplate, err := storeInstance.Render("Hello {{ .name }}", "user")` -func (storeInstance *Store) Render(templateSource, group string) (string, error) { - if err := storeInstance.ensureReady(opRender); err != nil { - return "", err +func (storeInstance *Store) Render(templateSource, group string) (string, core.Result) { + if result := storeInstance.ensureReady(opRender); !result.OK { + return "", result } templateData := make(map[string]string) for entry, err := range storeInstance.All(group) { if err != nil { - return "", core.E(opRender, "iterate rows", err) + return "", core.Fail(core.E(opRender, "iterate rows", err)) } templateData[entry.Key] = entry.Value } renderTemplate, err := template.New("render").Parse(templateSource) if err != nil { - return "", core.E(opRender, "parse template", err) + return "", core.Fail(core.E(opRender, "parse template", err)) } builder := core.NewBuilder() if err := renderTemplate.Execute(builder, templateData); err != nil { - return "", core.E(opRender, "execute template", err) + return "", core.Fail(core.E(opRender, "execute template", err)) } - return builder.String(), nil + return builder.String(), core.Ok(nil) } // Usage example: `tenantKeyCount, err := storeInstance.CountAll("tenant-a:")` -func (storeInstance *Store) CountAll(groupPrefix string) (int, error) { - if err := storeInstance.ensureReady("store.CountAll"); err != nil { - return 0, err +func (storeInstance *Store) CountAll(groupPrefix string) (int, core.Result) { + if result := storeInstance.ensureReady("store.CountAll"); !result.OK { + return 0, result } var count int @@ -979,26 +997,26 @@ func (storeInstance *Store) CountAll(groupPrefix string) (int, error) { ).Scan(&count) } if err != nil { - return 0, core.E("store.CountAll", "count rows", err) + return 0, core.Fail(core.E("store.CountAll", "count rows", err)) } - return count, nil + return count, core.Ok(nil) } // Usage example: `tenantGroupNames, err := storeInstance.Groups("tenant-a:")` // Usage example: `allGroupNames, err := storeInstance.Groups()` -func (storeInstance *Store) Groups(groupPrefix ...string) ([]string, error) { - if err := storeInstance.ensureReady("store.Groups"); err != nil { - return nil, err +func (storeInstance *Store) Groups(groupPrefix ...string) ([]string, core.Result) { + if result := storeInstance.ensureReady("store.Groups"); !result.OK { + return nil, result } var groupNames []string for groupName, err := range storeInstance.GroupsSeq(groupPrefix...) { if err != nil { - return nil, err + return nil, core.Fail(err) } groupNames = append(groupNames, groupName) } - return groupNames, nil + return groupNames, core.Ok(nil) } // Usage example: `for tenantGroupName, err := range storeInstance.GroupsSeq("tenant-a:") { if err != nil { break }; fmt.Println(tenantGroupName) }` @@ -1006,7 +1024,8 @@ func (storeInstance *Store) Groups(groupPrefix ...string) ([]string, error) { func (storeInstance *Store) GroupsSeq(groupPrefix ...string) iter.Seq2[string, error] { actualGroupPrefix := firstStringOrEmpty(groupPrefix) return func(yield func(string, error) bool) { - if err := storeInstance.ensureReady(opGroupsSeq); err != nil { + if result := storeInstance.ensureReady(opGroupsSeq); !result.OK { + err, _ := result.Value.(error) yield("", err) return } @@ -1070,18 +1089,19 @@ func escapeLike(text string) string { } // Usage example: `removed, err := storeInstance.PurgeExpired()` -func (storeInstance *Store) PurgeExpired() (int64, error) { - if err := storeInstance.ensureReady("store.PurgeExpired"); err != nil { - return 0, err +func (storeInstance *Store) PurgeExpired() (int64, core.Result) { + if result := storeInstance.ensureReady("store.PurgeExpired"); !result.OK { + return 0, result } cutoffUnixMilli := time.Now().UnixMilli() - expiredEntries, err := deleteExpiredEntriesMatchingGroupPrefix(storeInstance.sqliteDatabase, "", cutoffUnixMilli) - if err != nil { - return 0, core.E("store.PurgeExpired", "delete expired rows", err) + expiredEntries, result := deleteExpiredEntriesMatchingGroupPrefix(storeInstance.sqliteDatabase, "", cutoffUnixMilli) + if !result.OK { + err, _ := result.Value.(error) + return 0, core.Fail(core.E("store.PurgeExpired", "delete expired rows", err)) } storeInstance.notifyExpiredEntries(expiredEntries) - return int64(len(expiredEntries)), nil + return int64(len(expiredEntries)), core.Ok(nil) } func (storeInstance *Store) notifyExpiredEntries(expiredEntries []expiredEntryRef) { @@ -1113,10 +1133,10 @@ func (storeInstance *Store) startBackgroundPurge() { case <-storeInstance.purgeContext.Done(): return case <-ticker.C: - if _, err := storeInstance.PurgeExpired(); err != nil { + if _, result := storeInstance.PurgeExpired(); !result.OK { // For example, a logger could record the failure here. The loop // keeps running so the next tick can retry. - _ = err + core.Error("background purge failed", "err", result.Error()) } } } @@ -1174,7 +1194,7 @@ func eventFromExpiredEntry(expiredEntry expiredEntryRef) Event { } } -func deleteExpiredEntriesMatchingGroupPrefix(database schemaDatabase, groupPrefix string, cutoffUnixMilli int64) ([]expiredEntryRef, error) { +func deleteExpiredEntriesMatchingGroupPrefix(database schemaDatabase, groupPrefix string, cutoffUnixMilli int64) ([]expiredEntryRef, core.Result) { var ( rows *sql.Rows err error @@ -1191,7 +1211,7 @@ func deleteExpiredEntriesMatchingGroupPrefix(database schemaDatabase, groupPrefi ) } if err != nil { - return nil, err + return nil, core.Fail(err) } defer func() { _ = rows.Close() }() @@ -1199,14 +1219,14 @@ func deleteExpiredEntriesMatchingGroupPrefix(database schemaDatabase, groupPrefi for rows.Next() { var expiredEntry expiredEntryRef if err := rows.Scan(&expiredEntry.group, &expiredEntry.key); err != nil { - return nil, err + return nil, core.Fail(err) } expiredEntries = append(expiredEntries, expiredEntry) } if err := rows.Err(); err != nil { - return nil, err + return nil, core.Fail(err) } - return expiredEntries, nil + return expiredEntries, core.Ok(nil) } type schemaDatabase interface { @@ -1225,91 +1245,95 @@ const createEntriesTableSQL = `CREATE TABLE IF NOT EXISTS entries ( // ensureSchema creates the current entries table and migrates the legacy // key-value table when present. -func ensureSchema(database *sql.DB) error { - entriesTableExists, err := tableExists(database, entriesTableName) - if err != nil { - return core.E(opEnsureSchema, "schema", err) +func ensureSchema(database *sql.DB) core.Result { + entriesTableExists, result := tableExists(database, entriesTableName) + if !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opEnsureSchema, "schema", err)) } - legacyEntriesTableExists, err := tableExists(database, legacyKeyValueTableName) - if err != nil { - return core.E(opEnsureSchema, "schema", err) + legacyEntriesTableExists, result := tableExists(database, legacyKeyValueTableName) + if !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opEnsureSchema, "schema", err)) } if entriesTableExists { - if err := ensureExpiryColumn(database); err != nil { - return core.E(opEnsureSchema, "migration", err) + if result := ensureExpiryColumn(database); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opEnsureSchema, "migration", err)) } if legacyEntriesTableExists { - if err := migrateLegacyEntriesTable(database); err != nil { - return core.E(opEnsureSchema, "migration", err) + if result := migrateLegacyEntriesTable(database); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opEnsureSchema, "migration", err)) } } - return nil + return core.Ok(nil) } if legacyEntriesTableExists { - if err := migrateLegacyEntriesTable(database); err != nil { - return core.E(opEnsureSchema, "migration", err) + if result := migrateLegacyEntriesTable(database); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opEnsureSchema, "migration", err)) } - return nil + return core.Ok(nil) } if _, err := database.Exec(createEntriesTableSQL); err != nil { - return core.E(opEnsureSchema, "schema", err) + return core.Fail(core.E(opEnsureSchema, "schema", err)) } - return nil + return core.Ok(nil) } // ensureExpiryColumn adds the expiry column to the current entries table when // it was created before TTL support. -func ensureExpiryColumn(database schemaDatabase) error { - hasExpiryColumn, err := tableHasColumn(database, entriesTableName, "expires_at") - if err != nil { - return err +func ensureExpiryColumn(database schemaDatabase) core.Result { + hasExpiryColumn, result := tableHasColumn(database, entriesTableName, "expires_at") + if !result.OK { + return result } if hasExpiryColumn { - return nil + return core.Ok(nil) } if _, err := database.Exec("ALTER TABLE " + entriesTableName + " ADD COLUMN expires_at INTEGER"); err != nil { if !core.Contains(err.Error(), "duplicate column name") { - return err + return core.Fail(err) } } - return nil + return core.Ok(nil) } // migrateLegacyEntriesTable copies rows from the old key-value table into the // descriptive entries schema and then removes the legacy table. -func migrateLegacyEntriesTable(database *sql.DB) error { +func migrateLegacyEntriesTable(database *sql.DB) core.Result { transaction, err := database.Begin() if err != nil { - return err + return core.Fail(err) } committed := false defer func() { if !committed { if rollbackErr := transaction.Rollback(); rollbackErr != nil { - // Ignore rollback failures; the original error is already being returned. - _ = rollbackErr + core.Error("legacy entries migration rollback failed", "err", rollbackErr) } } }() - entriesTableExists, err := tableExists(transaction, entriesTableName) - if err != nil { - return err + entriesTableExists, result := tableExists(transaction, entriesTableName) + if !result.OK { + return result } if !entriesTableExists { if _, err := transaction.Exec(createEntriesTableSQL); err != nil { - return err + return core.Fail(err) } } - legacyHasExpiryColumn, err := tableHasColumn(transaction, legacyKeyValueTableName, "expires_at") - if err != nil { - return err + legacyHasExpiryColumn, result := tableHasColumn(transaction, legacyKeyValueTableName, "expires_at") + if !result.OK { + return result } insertSQL := "INSERT OR IGNORE INTO " + entriesTableName + " (" + entryGroupColumn + ", " + entryKeyColumn + ", " + entryValueColumn + ", expires_at) SELECT grp, key, value, NULL FROM " + legacyKeyValueTableName @@ -1317,37 +1341,37 @@ func migrateLegacyEntriesTable(database *sql.DB) error { insertSQL = "INSERT OR IGNORE INTO " + entriesTableName + " (" + entryGroupColumn + ", " + entryKeyColumn + ", " + entryValueColumn + ", expires_at) SELECT grp, key, value, expires_at FROM " + legacyKeyValueTableName } if _, err := transaction.Exec(insertSQL); err != nil { - return err + return core.Fail(err) } if _, err := transaction.Exec("DROP TABLE " + legacyKeyValueTableName); err != nil { - return err + return core.Fail(err) } if err := transaction.Commit(); err != nil { - return err + return core.Fail(err) } committed = true - return nil + return core.Ok(nil) } -func tableExists(database schemaDatabase, tableName string) (bool, error) { +func tableExists(database schemaDatabase, tableName string) (bool, core.Result) { var existingTableName string err := database.QueryRow( "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", tableName, ).Scan(&existingTableName) if err == sql.ErrNoRows { - return false, nil + return false, core.Ok(nil) } if err != nil { - return false, err + return false, core.Fail(err) } - return true, nil + return true, core.Ok(nil) } -func tableHasColumn(database schemaDatabase, tableName, columnName string) (bool, error) { +func tableHasColumn(database schemaDatabase, tableName, columnName string) (bool, core.Result) { rows, err := database.Query("PRAGMA table_info(" + tableName + ")") if err != nil { - return false, err + return false, core.Fail(err) } defer func() { _ = rows.Close() }() @@ -1361,14 +1385,14 @@ func tableHasColumn(database schemaDatabase, tableName, columnName string) (bool primaryKey int ) if err := rows.Scan(&columnID, &name, &columnType, ¬Null, &defaultValue, &primaryKey); err != nil { - return false, err + return false, core.Fail(err) } if name == columnName { - return true, nil + return true, core.Ok(nil) } } if err := rows.Err(); err != nil { - return false, err + return false, core.Fail(err) } - return false, nil + return false, core.Ok(nil) } diff --git a/go/store_example_test.go b/go/store_example_test.go new file mode 100644 index 0000000..be66c4e --- /dev/null +++ b/go/store_example_test.go @@ -0,0 +1,338 @@ +package store + +import ( + "time" + + core "dappco.re/go" +) + +func exampleRequireOK(result core.Result) { + if !result.OK { + panic(result.Value) + } +} + +func exampleOpenStore() *Store { + storeInstance, result := New(":memory:") + exampleRequireOK(result) + return storeInstance +} + +func exampleOpenConfiguredStore() *Store { + storeInstance, result := NewConfigured(StoreConfig{ + DatabasePath: ":memory:", + PurgeInterval: time.Minute, + WorkspaceStateDirectory: ".core/example-state", + }) + exampleRequireOK(result) + return storeInstance +} + +func exampleCloseStore(storeInstance *Store) { + if storeInstance == nil { + return + } + result := storeInstance.Close() + exampleRequireOK(result) +} + +func exampleStoreWithConfig() *Store { + storeInstance := exampleOpenStore() + exampleRequireOK(storeInstance.Set("config", "colour", "blue")) + exampleRequireOK(storeInstance.Set("config", "tags", "alpha,beta")) + return storeInstance +} + +func exampleWorkspace() (*Store, *Workspace) { + storeInstance := exampleOpenConfiguredStore() + workspace, result := storeInstance.NewWorkspace("example-workspace") + exampleRequireOK(result) + return storeInstance, workspace +} + +func exampleCollectKeyValues(seq core.Seq2[KeyValue, error]) []KeyValue { + entries := []KeyValue{} + for entry, err := range seq { + if err != nil { + panic(err) + } + entries = append(entries, entry) + } + return entries +} + +func exampleCollectGroups(seq core.Seq2[string, error]) []string { + groups := []string{} + for group, err := range seq { + if err != nil { + panic(err) + } + groups = append(groups, group) + } + return groups +} + +func exampleCollectStrings(seq core.Seq[string]) []string { + values := []string{} + for value := range seq { + values = append(values, value) + } + return values +} + +func ExampleStoreConfig_Normalised() { + config := StoreConfig{DatabasePath: ":memory:"} + normalised := config.Normalised() + core.Println(normalised.PurgeInterval > 0) +} + +func ExampleStoreConfig_Validate() { + config := StoreConfig{DatabasePath: ":memory:"} + result := config.Validate() + core.Println(result.OK) +} + +func ExampleJournalConfiguration_Validate() { + config := JournalConfiguration{ + EndpointURL: "http://127.0.0.1:8086", + Organisation: "core", + BucketName: "events", + } + result := config.Validate() + core.Println(result.OK) +} + +func ExampleWithJournal() { + storeInstance, result := New(":memory:", WithJournal("http://127.0.0.1:8086", "core", "events")) + exampleRequireOK(result) + defer exampleCloseStore(storeInstance) + core.Println(storeInstance.JournalConfigured()) +} + +func ExampleWithWorkspaceStateDirectory() { + storeInstance, result := New(":memory:", WithWorkspaceStateDirectory(".core/workspaces")) + exampleRequireOK(result) + defer exampleCloseStore(storeInstance) + core.Println(storeInstance.WorkspaceStateDirectory()) +} + +func ExampleStore_JournalConfiguration() { + storeInstance, result := New(":memory:", WithJournal("http://127.0.0.1:8086", "core", "events")) + exampleRequireOK(result) + defer exampleCloseStore(storeInstance) + core.Println(storeInstance.JournalConfiguration().BucketName) +} + +func ExampleStore_JournalConfigured() { + storeInstance, result := New(":memory:", WithJournal("http://127.0.0.1:8086", "core", "events")) + exampleRequireOK(result) + defer exampleCloseStore(storeInstance) + core.Println(storeInstance.JournalConfigured()) +} + +func ExampleStore_Config() { + storeInstance := exampleOpenConfiguredStore() + defer exampleCloseStore(storeInstance) + core.Println(storeInstance.Config().WorkspaceStateDirectory) +} + +func ExampleStore_DatabasePath() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + core.Println(storeInstance.DatabasePath()) +} + +func ExampleStore_WorkspaceStateDirectory() { + storeInstance := exampleOpenConfiguredStore() + defer exampleCloseStore(storeInstance) + core.Println(storeInstance.WorkspaceStateDirectory()) +} + +func ExampleStore_IsClosed() { + storeInstance := exampleOpenStore() + result := storeInstance.Close() + exampleRequireOK(result) + core.Println(storeInstance.IsClosed()) +} + +func ExampleWithPurgeInterval() { + storeInstance, result := New(":memory:", WithPurgeInterval(time.Minute)) + exampleRequireOK(result) + defer exampleCloseStore(storeInstance) + core.Println(storeInstance.Config().PurgeInterval) +} + +func ExampleNewConfigured() { + storeInstance, result := NewConfigured(StoreConfig{DatabasePath: ":memory:"}) + exampleRequireOK(result) + defer exampleCloseStore(storeInstance) + core.Println(storeInstance.DatabasePath()) +} + +func ExampleNew() { + storeInstance, result := New(":memory:") + exampleRequireOK(result) + defer exampleCloseStore(storeInstance) + core.Println(storeInstance.IsClosed()) +} + +func ExampleStore_Close() { + storeInstance := exampleOpenStore() + result := storeInstance.Close() + exampleRequireOK(result) + core.Println(storeInstance.IsClosed()) +} + +func ExampleStore_Get() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + value, result := storeInstance.Get("config", "colour") + exampleRequireOK(result) + core.Println(value) +} + +func ExampleStore_Set() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Set("config", "colour", "blue") + exampleRequireOK(result) +} + +func ExampleStore_SetWithTTL() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.SetWithTTL("cache", "token", "abc", time.Minute) + exampleRequireOK(result) +} + +func ExampleStore_Delete() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + result := storeInstance.Delete("config", "colour") + exampleRequireOK(result) +} + +func ExampleStore_Exists() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + exists, result := storeInstance.Exists("config", "colour") + exampleRequireOK(result) + core.Println(exists) +} + +func ExampleStore_GroupExists() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + exists, result := storeInstance.GroupExists("config") + exampleRequireOK(result) + core.Println(exists) +} + +func ExampleStore_Count() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + count, result := storeInstance.Count("config") + exampleRequireOK(result) + core.Println(count) +} + +func ExampleStore_DeleteGroup() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + result := storeInstance.DeleteGroup("config") + exampleRequireOK(result) +} + +func ExampleStore_DeletePrefix() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + result := storeInstance.DeletePrefix("conf") + exampleRequireOK(result) +} + +func ExampleStore_GetAll() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + values, result := storeInstance.GetAll("config") + exampleRequireOK(result) + core.Println(values["colour"]) +} + +func ExampleStore_GetPage() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + entries, result := storeInstance.GetPage("config", 0, 10) + exampleRequireOK(result) + core.Println(len(entries)) +} + +func ExampleStore_AllSeq() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + entries := exampleCollectKeyValues(storeInstance.AllSeq("config")) + core.Println(len(entries)) +} + +func ExampleStore_All() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + entries := exampleCollectKeyValues(storeInstance.All("config")) + core.Println(len(entries)) +} + +func ExampleStore_GetSplit() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + seq, result := storeInstance.GetSplit("config", "tags", ",") + exampleRequireOK(result) + core.Println(exampleCollectStrings(seq)) +} + +func ExampleStore_GetFields() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + exampleRequireOK(storeInstance.Set("config", "text", "alpha beta")) + seq, result := storeInstance.GetFields("config", "text") + exampleRequireOK(result) + core.Println(exampleCollectStrings(seq)) +} + +func ExampleStore_Render() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + rendered, result := storeInstance.Render("colour={{.colour}}", "config") + exampleRequireOK(result) + core.Println(rendered) +} + +func ExampleStore_CountAll() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + count, result := storeInstance.CountAll("conf") + exampleRequireOK(result) + core.Println(count) +} + +func ExampleStore_Groups() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + groups, result := storeInstance.Groups() + exampleRequireOK(result) + core.Println(groups) +} + +func ExampleStore_GroupsSeq() { + storeInstance := exampleStoreWithConfig() + defer exampleCloseStore(storeInstance) + groups := exampleCollectGroups(storeInstance.GroupsSeq()) + core.Println(groups) +} + +func ExampleStore_PurgeExpired() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + exampleRequireOK(storeInstance.SetWithTTL("cache", "token", "abc", time.Nanosecond)) + removed, result := storeInstance.PurgeExpired() + exampleRequireOK(result) + core.Println(removed) +} diff --git a/store_test.go b/go/store_test.go similarity index 92% rename from store_test.go rename to go/store_test.go index 233aaf6..6ea2cee 100644 --- a/store_test.go +++ b/go/store_test.go @@ -79,7 +79,7 @@ func TestStore_New_Bad_ReadOnlyDir(t *testing.T) { _, err = New(databasePath) // May or may not fail depending on OS/filesystem — just exercise the code path. - if err != nil { + if !err.OK { assertContainsString(t, err.Error(), opNew) } } @@ -91,8 +91,8 @@ func TestStore_New_Good_WALMode(t *testing.T) { defer func() { _ = storeInstance.Close() }() var mode string - err = storeInstance.sqliteDatabase.QueryRow("PRAGMA journal_mode").Scan(&mode) - assertNoError(t, err) + scanErr := storeInstance.sqliteDatabase.QueryRow("PRAGMA journal_mode").Scan(&mode) + assertNoError(t, scanErr) assertEqualf(t, "wal", mode, "journal_mode should be WAL") } @@ -354,12 +354,13 @@ func TestStore_SetGet_Good(t *testing.T) { assertNoError(t, err) defer func() { _ = storeInstance.Close() }() - err = storeInstance.Set("config", "theme", "dark") + SetGet := "dark" + err = storeInstance.Set("config", "theme", SetGet) assertNoError(t, err) value, err := storeInstance.Get("config", "theme") assertNoError(t, err) - assertEqual(t, "dark", value) + assertEqual(t, SetGet, value) } func TestStore_Set_Good_Upsert(t *testing.T) { @@ -384,7 +385,7 @@ func TestStore_Get_Bad_NotFound(t *testing.T) { _, err := storeInstance.Get("config", "missing") assertError(t, err) - assertTruef(t, core.Is(err, NotFoundError), "should wrap NotFoundError") + assertTruef(t, errIs(errorValue(err), NotFoundError), "should wrap NotFoundError") } func TestStore_Get_Bad_NonExistentGroup(t *testing.T) { @@ -393,7 +394,7 @@ func TestStore_Get_Bad_NonExistentGroup(t *testing.T) { _, err := storeInstance.Get("no-such-group", "key") assertError(t, err) - assertTrue(t, core.Is(err, NotFoundError)) + assertTrue(t, errIs(errorValue(err), NotFoundError)) } func TestStore_Get_Bad_ClosedStore(t *testing.T) { @@ -903,7 +904,7 @@ func TestStore_GetSplit_Bad_MissingKey(t *testing.T) { _, err := storeInstance.GetSplit("g", "missing", ",") assertError(t, err) - assertTrue(t, core.Is(err, NotFoundError)) + assertTrue(t, errIs(errorValue(err), NotFoundError)) } func TestStore_GetFields_Good_SplitsWhitespace(t *testing.T) { @@ -947,7 +948,7 @@ func TestStore_GetFields_Bad_MissingKey(t *testing.T) { _, err := storeInstance.GetFields("g", "missing") assertError(t, err) - assertTrue(t, core.Is(err, NotFoundError)) + assertTrue(t, errIs(errorValue(err), NotFoundError)) } // --------------------------------------------------------------------------- @@ -1256,23 +1257,24 @@ func TestStore_GroupIsolation_Good(t *testing.T) { storeInstance, _ := New(testMemoryDatabasePath) defer func() { _ = storeInstance.Close() }() - assertNoError(t, storeInstance.Set("alpha", "k", "a-val")) - assertNoError(t, storeInstance.Set("beta", "k", "b-val")) + GroupIsolation := []string{"alpha", "beta"} + assertNoError(t, storeInstance.Set(GroupIsolation[0], "k", "a-val")) + assertNoError(t, storeInstance.Set(GroupIsolation[1], "k", "b-val")) - alphaValue, err := storeInstance.Get("alpha", "k") + alphaValue, err := storeInstance.Get(GroupIsolation[0], "k") assertNoError(t, err) assertEqual(t, "a-val", alphaValue) - betaValue, err := storeInstance.Get("beta", "k") + betaValue, err := storeInstance.Get(GroupIsolation[1], "k") assertNoError(t, err) assertEqual(t, "b-val", betaValue) // Delete from alpha should not affect beta. - assertNoError(t, storeInstance.Delete("alpha", "k")) - _, err = storeInstance.Get("alpha", "k") + assertNoError(t, storeInstance.Delete(GroupIsolation[0], "k")) + _, err = storeInstance.Get(GroupIsolation[0], "k") assertError(t, err) - betaValueAfterDelete, err := storeInstance.Get("beta", "k") + betaValueAfterDelete, err := storeInstance.Get(GroupIsolation[1], "k") assertNoError(t, err) assertEqual(t, "b-val", betaValueAfterDelete) } @@ -1312,7 +1314,8 @@ func startConcurrentStoreWriters(storeInstance *Store, goroutines, opsPerGorouti for i := range opsPerGoroutine { key := core.Sprintf(testKeyFormat, i) value := core.Sprintf("val-%d-%d", id, i) - if err := storeInstance.Set(group, key, value); err != nil { + if result := storeInstance.Set(group, key, value); !result.OK { + err, _ := result.Value.(error) recordedErrors <- core.E("TestStore_Concurrent_Good_ReadWrite", core.Sprintf("writer %d", id), err) } } @@ -1328,8 +1331,9 @@ func startConcurrentStoreReaders(storeInstance *Store, goroutines, opsPerGorouti group := core.Sprintf(testGroupFormat, id) for i := range opsPerGoroutine { key := core.Sprintf(testKeyFormat, i) - _, err := storeInstance.Get(group, key) - if err != nil && !core.Is(err, NotFoundError) { + _, result := storeInstance.Get(group, key) + err, _ := result.Value.(error) + if !result.OK && !errIs(errorValue(err), NotFoundError) { recordedErrors <- core.E("TestStore_Concurrent_Good_ReadWrite", core.Sprintf("reader %d", id), err) } } @@ -1369,9 +1373,9 @@ func TestStore_Concurrent_Good_GetAll(t *testing.T) { var waitGroup sync.WaitGroup for range 10 { waitGroup.Go(func() { - all, err := storeInstance.GetAll("shared") - if err != nil { - t.Errorf("GetAll failed: %v", err) + all, result := storeInstance.GetAll("shared") + if !result.OK { + t.Errorf("GetAll failed: %v", result.Error()) return } if len(all) != 50 { @@ -1412,7 +1416,7 @@ func TestStore_NotFoundError_Good_Is(t *testing.T) { _, err := storeInstance.Get("g", "k") assertError(t, err) - assertTruef(t, core.Is(err, NotFoundError), "error should be NotFoundError via core.Is") + assertTruef(t, errIs(errorValue(err), NotFoundError), "error should be NotFoundError via core.Is") assertContainsString(t, err.Error(), "g/k") } @@ -1516,7 +1520,7 @@ func TestStore_SetWithTTL_Good_ExpiresOnGet(t *testing.T) { _, err := storeInstance.Get("g", "ephemeral") assertError(t, err) - assertTruef(t, core.Is(err, NotFoundError), "expired key should be NotFoundError") + assertTruef(t, errIs(errorValue(err), NotFoundError), "expired key should be NotFoundError") } func TestStore_SetWithTTL_Good_ExpiresOnGetEmitsDeleteEvent(t *testing.T) { @@ -1538,7 +1542,7 @@ func TestStore_SetWithTTL_Good_ExpiresOnGetEmitsDeleteEvent(t *testing.T) { _, err = storeInstance.Get("g", "ephemeral") assertError(t, err) - assertTruef(t, core.Is(err, NotFoundError), "expired key should be NotFoundError") + assertTruef(t, errIs(errorValue(err), NotFoundError), "expired key should be NotFoundError") select { case event := <-events: @@ -1733,8 +1737,8 @@ func TestStore_PurgeExpired_Good_BackgroundPurge(t *testing.T) { // The expired key should have been removed by the background goroutine. // Use a raw query to check the row is actually gone (not just filtered by Get). var count int - err = storeInstance.sqliteDatabase.QueryRow("SELECT COUNT(*) FROM entries WHERE group_name = ?", "g").Scan(&count) - assertNoError(t, err) + scanErr := storeInstance.sqliteDatabase.QueryRow("SELECT COUNT(*) FROM entries WHERE group_name = ?", "g").Scan(&count) + assertNoError(t, scanErr) assertEqualf(t, 1, count, "background purge should have deleted the expired row") } @@ -1937,7 +1941,11 @@ func TestStore_StoreConfig_Normalised_Good(t *T) { } func TestStore_StoreConfig_Normalised_Bad(t *T) { - AssertError(t, emptyStoreConfigValidationError()) + config := StoreConfig{} + normalised := config.Normalised() + result := normalised.Validate() + AssertError(t, result) + AssertEqual(t, "", normalised.DatabasePath) } func TestStore_StoreConfig_Normalised_Ugly(t *T) { @@ -1953,13 +1961,10 @@ func TestStore_StoreConfig_Validate_Good(t *T) { } func TestStore_StoreConfig_Validate_Bad(t *T) { - err := emptyStoreConfigValidationError() - AssertError(t, err) -} - -func emptyStoreConfigValidationError() error { config := StoreConfig{DatabasePath: ""} - return config.Validate() + result := config.Validate() + AssertError(t, result) + AssertEqual(t, "", config.DatabasePath) } func TestStore_StoreConfig_Validate_Ugly(t *T) { @@ -1987,7 +1992,9 @@ func TestStore_JournalConfiguration_Validate_Ugly(t *T) { } func TestStore_WithJournal_Good(t *T) { - storeInstance := requireJournalConfiguredStore(t) + option := WithJournal(testJournalEndpoint, "core", "events") + storeInstance, err := New(testMemoryDatabasePath, option) + RequireNoError(t, err) defer func() { _ = storeInstance.Close() }() AssertTrue(t, storeInstance.JournalConfigured()) } @@ -2079,7 +2086,7 @@ func TestStore_New_Ugly(t *T) { } func TestStore_Store_JournalConfiguration_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) config := storeInstance.JournalConfiguration() AssertEqual(t, JournalConfiguration{}, config) } @@ -2119,13 +2126,13 @@ func TestStore_Store_JournalConfigured_Bad(t *T) { } func TestStore_Store_JournalConfigured_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) configured := storeInstance.JournalConfigured() AssertFalse(t, configured) } func TestStore_Store_Config_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) config := storeInstance.Config() AssertEqual(t, testMemoryDatabasePath, config.DatabasePath) } @@ -2137,7 +2144,7 @@ func TestStore_Store_Config_Bad(t *T) { } func TestStore_Store_Config_Ugly(t *T) { - medium := newAX7Medium() + medium := newFixtureMedium() storeInstance, err := NewConfigured(StoreConfig{DatabasePath: testMemoryDatabasePath, Medium: medium}) RequireNoError(t, err) defer func() { _ = storeInstance.Close() }() @@ -2145,7 +2152,7 @@ func TestStore_Store_Config_Ugly(t *T) { } func TestStore_Store_DatabasePath_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) path := storeInstance.DatabasePath() AssertEqual(t, testMemoryDatabasePath, path) } @@ -2165,7 +2172,7 @@ func TestStore_Store_DatabasePath_Ugly(t *T) { } func TestStore_Store_WorkspaceStateDirectory_Good(t *T) { - storeInstance, stateDirectory := ax7ConfiguredStore(t) + storeInstance, stateDirectory := fixtureConfiguredStore(t) path := storeInstance.WorkspaceStateDirectory() AssertEqual(t, stateDirectory, path) } @@ -2184,7 +2191,7 @@ func TestStore_Store_WorkspaceStateDirectory_Ugly(t *T) { } func TestStore_Store_IsClosed_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) closed := storeInstance.IsClosed() AssertFalse(t, closed) } @@ -2224,7 +2231,7 @@ func TestStore_Store_Close_Ugly(t *T) { } func TestStore_Store_Get_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "colour", "blue")) got, err := storeInstance.Get("config", "colour") AssertNoError(t, err) @@ -2232,14 +2239,14 @@ func TestStore_Store_Get_Good(t *T) { } func TestStore_Store_Get_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) _, err := storeInstance.Get("missing", "key") AssertErrorIs(t, err, NotFoundError) AssertContains(t, err.Error(), "missing/key") } func TestStore_Store_Get_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("", "", "empty")) got, err := storeInstance.Get("", "") AssertNoError(t, err) @@ -2247,43 +2254,43 @@ func TestStore_Store_Get_Ugly(t *T) { } func TestStore_Store_Set_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) err := storeInstance.Set("config", "colour", "blue") AssertNoError(t, err) - AssertEqual(t, "blue", ax7MustGet(t, storeInstance, "config", "colour")) + AssertEqual(t, "blue", fixtureMustGet(t, storeInstance, "config", "colour")) } func TestStore_Store_Set_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) err := storeInstance.Set("config", "colour", "blue") AssertError(t, err) } func TestStore_Store_Set_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "colour", "blue")) err := storeInstance.Set("config", "colour", "green") AssertNoError(t, err) - AssertEqual(t, "green", ax7MustGet(t, storeInstance, "config", "colour")) + AssertEqual(t, "green", fixtureMustGet(t, storeInstance, "config", "colour")) } func TestStore_Store_SetWithTTL_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) err := storeInstance.SetWithTTL("session", "token", "abc", Hour) AssertNoError(t, err) - AssertEqual(t, "abc", ax7MustGet(t, storeInstance, "session", "token")) + AssertEqual(t, "abc", fixtureMustGet(t, storeInstance, "session", "token")) } func TestStore_Store_SetWithTTL_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) err := storeInstance.SetWithTTL("session", "token", "abc", Hour) AssertError(t, err) } func TestStore_Store_SetWithTTL_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) err := storeInstance.SetWithTTL("session", "token", "abc", -Millisecond) AssertNoError(t, err) _, getErr := storeInstance.Get("session", "token") @@ -2291,7 +2298,7 @@ func TestStore_Store_SetWithTTL_Ugly(t *T) { } func TestStore_Store_Delete_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "colour", "blue")) err := storeInstance.Delete("config", "colour") AssertNoError(t, err) @@ -2300,21 +2307,21 @@ func TestStore_Store_Delete_Good(t *T) { } func TestStore_Store_Delete_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) err := storeInstance.Delete("config", "colour") AssertError(t, err) } func TestStore_Store_Delete_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) err := storeInstance.Delete("missing", "key") AssertNoError(t, err) - AssertFalse(t, ax7MustExists(t, storeInstance, "missing", "key")) + AssertFalse(t, fixtureMustExists(t, storeInstance, "missing", "key")) } func TestStore_Store_Exists_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "colour", "blue")) exists, err := storeInstance.Exists("config", "colour") AssertNoError(t, err) @@ -2322,7 +2329,7 @@ func TestStore_Store_Exists_Good(t *T) { } func TestStore_Store_Exists_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) exists, err := storeInstance.Exists("config", "colour") AssertError(t, err) @@ -2330,14 +2337,14 @@ func TestStore_Store_Exists_Bad(t *T) { } func TestStore_Store_Exists_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) exists, err := storeInstance.Exists("missing", "key") AssertNoError(t, err) AssertFalse(t, exists) } func TestStore_Store_GroupExists_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "colour", "blue")) exists, err := storeInstance.GroupExists("config") AssertNoError(t, err) @@ -2345,7 +2352,7 @@ func TestStore_Store_GroupExists_Good(t *T) { } func TestStore_Store_GroupExists_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) exists, err := storeInstance.GroupExists("config") AssertError(t, err) @@ -2353,14 +2360,14 @@ func TestStore_Store_GroupExists_Bad(t *T) { } func TestStore_Store_GroupExists_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) exists, err := storeInstance.GroupExists("empty") AssertNoError(t, err) AssertFalse(t, exists) } func TestStore_Store_Count_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "a", "1")) count, err := storeInstance.Count("config") AssertNoError(t, err) @@ -2368,7 +2375,7 @@ func TestStore_Store_Count_Good(t *T) { } func TestStore_Store_Count_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) count, err := storeInstance.Count("config") AssertError(t, err) @@ -2376,59 +2383,59 @@ func TestStore_Store_Count_Bad(t *T) { } func TestStore_Store_Count_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) count, err := storeInstance.Count("missing") AssertNoError(t, err) AssertEqual(t, 0, count) } func TestStore_Store_DeleteGroup_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "a", "1")) err := storeInstance.DeleteGroup("config") AssertNoError(t, err) - AssertFalse(t, ax7MustGroupExists(t, storeInstance, "config")) + AssertFalse(t, fixtureMustGroupExists(t, storeInstance, "config")) } func TestStore_Store_DeleteGroup_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) err := storeInstance.DeleteGroup("config") AssertError(t, err) } func TestStore_Store_DeleteGroup_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) err := storeInstance.DeleteGroup("missing") AssertNoError(t, err) - AssertFalse(t, ax7MustGroupExists(t, storeInstance, "missing")) + AssertFalse(t, fixtureMustGroupExists(t, storeInstance, "missing")) } func TestStore_Store_DeletePrefix_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set(testTenantAConfigGroup, "a", "1")) err := storeInstance.DeletePrefix(testTenantAPrefix) AssertNoError(t, err) - AssertFalse(t, ax7MustGroupExists(t, storeInstance, testTenantAConfigGroup)) + AssertFalse(t, fixtureMustGroupExists(t, storeInstance, testTenantAConfigGroup)) } func TestStore_Store_DeletePrefix_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) err := storeInstance.DeletePrefix(testTenantAPrefix) AssertError(t, err) } func TestStore_Store_DeletePrefix_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("one", "a", "1")) err := storeInstance.DeletePrefix("") AssertNoError(t, err) - AssertFalse(t, ax7MustGroupExists(t, storeInstance, "one")) + AssertFalse(t, fixtureMustGroupExists(t, storeInstance, "one")) } func TestStore_Store_GetAll_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "a", "1")) entries, err := storeInstance.GetAll("config") AssertNoError(t, err) @@ -2436,7 +2443,7 @@ func TestStore_Store_GetAll_Good(t *T) { } func TestStore_Store_GetAll_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) entries, err := storeInstance.GetAll("config") AssertError(t, err) @@ -2444,14 +2451,14 @@ func TestStore_Store_GetAll_Bad(t *T) { } func TestStore_Store_GetAll_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) entries, err := storeInstance.GetAll("missing") AssertNoError(t, err) AssertEmpty(t, entries) } func TestStore_Store_GetPage_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "a", "1")) page, err := storeInstance.GetPage("config", 0, 1) AssertNoError(t, err) @@ -2459,113 +2466,113 @@ func TestStore_Store_GetPage_Good(t *T) { } func TestStore_Store_GetPage_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) page, err := storeInstance.GetPage("config", -1, 1) AssertError(t, err) AssertNil(t, page) } func TestStore_Store_GetPage_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) page, err := storeInstance.GetPage("missing", 0, 10) AssertNoError(t, err) AssertEmpty(t, page) } func TestStore_Store_AllSeq_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "a", "1")) - entries, err := ax7CollectKeyValues(storeInstance.AllSeq("config")) + entries, err := fixtureCollectKeyValues(storeInstance.AllSeq("config")) AssertNoError(t, err) AssertEqual(t, "a", entries[0].Key) } func TestStore_Store_AllSeq_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) - entries, err := ax7CollectKeyValues(storeInstance.AllSeq("config")) + entries, err := fixtureCollectKeyValues(storeInstance.AllSeq("config")) AssertError(t, err) AssertEmpty(t, entries) } func TestStore_Store_AllSeq_Ugly(t *T) { - storeInstance := ax7Store(t) - entries, err := ax7CollectKeyValues(storeInstance.AllSeq("missing")) + storeInstance := fixtureStore(t) + entries, err := fixtureCollectKeyValues(storeInstance.AllSeq("missing")) AssertNoError(t, err) AssertEmpty(t, entries) } func TestStore_Store_All_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "a", "1")) - entries, err := ax7CollectKeyValues(storeInstance.All("config")) + entries, err := fixtureCollectKeyValues(storeInstance.All("config")) AssertNoError(t, err) AssertEqual(t, "1", entries[0].Value) } func TestStore_Store_All_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) - entries, err := ax7CollectKeyValues(storeInstance.All("config")) + entries, err := fixtureCollectKeyValues(storeInstance.All("config")) AssertError(t, err) AssertEmpty(t, entries) } func TestStore_Store_All_Ugly(t *T) { - storeInstance := ax7Store(t) - entries, err := ax7CollectKeyValues(storeInstance.All("missing")) + storeInstance := fixtureStore(t) + entries, err := fixtureCollectKeyValues(storeInstance.All("missing")) AssertNoError(t, err) AssertEmpty(t, entries) } func TestStore_Store_GetSplit_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "hosts", "a,b")) seq, err := storeInstance.GetSplit("config", "hosts", ",") AssertNoError(t, err) - AssertEqual(t, []string{"a", "b"}, ax7CollectStrings(seq)) + AssertEqual(t, []string{"a", "b"}, fixtureCollectStrings(seq)) } func TestStore_Store_GetSplit_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) seq, err := storeInstance.GetSplit("missing", "hosts", ",") AssertError(t, err) AssertNil(t, seq) } func TestStore_Store_GetSplit_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "hosts", "ab")) seq, err := storeInstance.GetSplit("config", "hosts", "") AssertNoError(t, err) - AssertEqual(t, []string{"a", "b"}, ax7CollectStrings(seq)) + AssertEqual(t, []string{"a", "b"}, fixtureCollectStrings(seq)) } func TestStore_Store_GetFields_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "flags", "a b")) seq, err := storeInstance.GetFields("config", "flags") AssertNoError(t, err) - AssertEqual(t, []string{"a", "b"}, ax7CollectStrings(seq)) + AssertEqual(t, []string{"a", "b"}, fixtureCollectStrings(seq)) } func TestStore_Store_GetFields_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) seq, err := storeInstance.GetFields("missing", "flags") AssertError(t, err) AssertNil(t, seq) } func TestStore_Store_GetFields_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "flags", " a b ")) seq, err := storeInstance.GetFields("config", "flags") AssertNoError(t, err) - AssertEqual(t, []string{"a", "b"}, ax7CollectStrings(seq)) + AssertEqual(t, []string{"a", "b"}, fixtureCollectStrings(seq)) } func TestStore_Store_Render_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "name", "alice")) rendered, err := storeInstance.Render("hello {{ .name }}", "config") AssertNoError(t, err) @@ -2573,21 +2580,21 @@ func TestStore_Store_Render_Good(t *T) { } func TestStore_Store_Render_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) rendered, err := storeInstance.Render("{{", "config") AssertError(t, err) AssertEqual(t, "", rendered) } func TestStore_Store_Render_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) rendered, err := storeInstance.Render("empty", "missing") AssertNoError(t, err) AssertEqual(t, "empty", rendered) } func TestStore_Store_CountAll_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set(testTenantAConfigGroup, "a", "1")) count, err := storeInstance.CountAll(testTenantAPrefix) AssertNoError(t, err) @@ -2595,7 +2602,7 @@ func TestStore_Store_CountAll_Good(t *T) { } func TestStore_Store_CountAll_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) count, err := storeInstance.CountAll(testTenantAPrefix) AssertError(t, err) @@ -2603,14 +2610,14 @@ func TestStore_Store_CountAll_Bad(t *T) { } func TestStore_Store_CountAll_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) count, err := storeInstance.CountAll("missing") AssertNoError(t, err) AssertEqual(t, 0, count) } func TestStore_Store_Groups_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "a", "1")) groups, err := storeInstance.Groups() AssertNoError(t, err) @@ -2618,7 +2625,7 @@ func TestStore_Store_Groups_Good(t *T) { } func TestStore_Store_Groups_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) groups, err := storeInstance.Groups() AssertError(t, err) @@ -2626,37 +2633,37 @@ func TestStore_Store_Groups_Bad(t *T) { } func TestStore_Store_Groups_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) groups, err := storeInstance.Groups("missing") AssertNoError(t, err) AssertEmpty(t, groups) } func TestStore_Store_GroupsSeq_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Set("config", "a", "1")) - groups, err := ax7CollectGroups(storeInstance.GroupsSeq()) + groups, err := fixtureCollectGroups(storeInstance.GroupsSeq()) AssertNoError(t, err) AssertEqual(t, []string{"config"}, groups) } func TestStore_Store_GroupsSeq_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) - groups, err := ax7CollectGroups(storeInstance.GroupsSeq()) + groups, err := fixtureCollectGroups(storeInstance.GroupsSeq()) AssertError(t, err) AssertEmpty(t, groups) } func TestStore_Store_GroupsSeq_Ugly(t *T) { - storeInstance := ax7Store(t) - groups, err := ax7CollectGroups(storeInstance.GroupsSeq("missing")) + storeInstance := fixtureStore(t) + groups, err := fixtureCollectGroups(storeInstance.GroupsSeq("missing")) AssertNoError(t, err) AssertEmpty(t, groups) } func TestStore_Store_PurgeExpired_Good(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.SetWithTTL("session", "token", "abc", -Millisecond)) removed, err := storeInstance.PurgeExpired() AssertNoError(t, err) @@ -2664,7 +2671,7 @@ func TestStore_Store_PurgeExpired_Good(t *T) { } func TestStore_Store_PurgeExpired_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) RequireNoError(t, storeInstance.Close()) removed, err := storeInstance.PurgeExpired() AssertError(t, err) @@ -2672,7 +2679,7 @@ func TestStore_Store_PurgeExpired_Bad(t *T) { } func TestStore_Store_PurgeExpired_Ugly(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) removed, err := storeInstance.PurgeExpired() AssertNoError(t, err) AssertEqual(t, int64(0), removed) diff --git a/test_asserts_test.go b/go/test_asserts_test.go similarity index 86% rename from test_asserts_test.go rename to go/test_asserts_test.go index c67341f..e6559d5 100644 --- a/test_asserts_test.go +++ b/go/test_asserts_test.go @@ -8,30 +8,31 @@ import ( core "dappco.re/go" ) -func assertNoError(t testing.TB, err error) { +func assertNoError(t testing.TB, err any) { t.Helper() - if err != nil { - t.Fatalf("unexpected error: %v", err) + if hasError(err) { + t.Fatalf("unexpected error: %v", errorText(err)) } } -func assertNoErrorf(t testing.TB, err error, format string, args ...any) { +func assertNoErrorf(t testing.TB, err any, format string, args ...any) { t.Helper() - if err != nil { - t.Fatalf("unexpected error: %v — "+format, append([]any{err}, args...)...) + if hasError(err) { + t.Fatalf("unexpected error: %v — "+format, append([]any{errorText(err)}, args...)...) } } -func assertError(t testing.TB, err error) { +func assertError(t testing.TB, err any) { t.Helper() - if err == nil { + if !hasError(err) { t.Fatal("expected error, got nil") } } -func assertErrorIs(t testing.TB, err, target error) { +func assertErrorIs(t testing.TB, err any, target error) { t.Helper() - if !errIs(err, target) { + actual := errorValue(err) + if !errIs(actual, target) { t.Fatalf("expected error matching %v, got %v", target, err) } } @@ -192,6 +193,46 @@ func errIs(err, target error) bool { return core.Is(err, target) } +func hasError(value any) bool { + switch typed := value.(type) { + case nil: + return false + case core.Result: + return !typed.OK + case error: + return typed != nil + default: + return false + } +} + +func errorValue(value any) error { + switch typed := value.(type) { + case nil: + return nil + case core.Result: + if typed.OK { + return nil + } + if err, ok := typed.Value.(error); ok { + return err + } + return core.E("store.test", typed.Error(), nil) + case error: + return typed + default: + return nil + } +} + +func errorText(value any) string { + err := errorValue(value) + if err == nil { + return "" + } + return err.Error() +} + func isNil(value any) bool { if value == nil { return true diff --git a/test_helpers_test.go b/go/test_helpers_test.go similarity index 98% rename from test_helpers_test.go rename to go/test_helpers_test.go index 8e19414..eb3c4f0 100644 --- a/test_helpers_test.go +++ b/go/test_helpers_test.go @@ -55,7 +55,7 @@ const ( testWorkspaceCommitFailedFormat = "workspace commit failed: %v" testWorkspaceEntryInsertSuffix = " (entry_kind, entry_data, created_at) VALUES (?, ?, ?)" testSQLInsertIntoPrefix = "INSERT INTO " - testAX7WorkspaceName = "ax7-workspace" + testFixtureWorkspaceName = "ax7-workspace" testHFDatasetID = "user/dataset" testCompactFailedFormat = "compact failed: %v" testUnexpectedArchivePathTypeFormat = "unexpected archive path type: %T" diff --git a/transaction.go b/go/transaction.go similarity index 65% rename from transaction.go rename to go/transaction.go index a9f37d7..1623d3b 100644 --- a/transaction.go +++ b/go/transaction.go @@ -18,17 +18,17 @@ type StoreTransaction struct { } // Usage example: `err := storeInstance.Transaction(func(transaction *store.StoreTransaction) error { if err := transaction.Set("tenant-a:config", "colour", "blue"); err != nil { return err }; return transaction.Set("tenant-b:config", "language", "en-GB") })` -func (storeInstance *Store) Transaction(operation func(*StoreTransaction) error) error { - if err := storeInstance.ensureReady(opTransaction); err != nil { - return err +func (storeInstance *Store) Transaction(operation func(*StoreTransaction) core.Result) core.Result { + if result := storeInstance.ensureReady(opTransaction); !result.OK { + return result } if operation == nil { - return core.E(opTransaction, "operation is nil", nil) + return core.Fail(core.E(opTransaction, "operation is nil", nil)) } transaction, err := storeInstance.sqliteDatabase.Begin() if err != nil { - return core.E(opTransaction, "begin transaction", err) + return core.Fail(core.E(opTransaction, "begin transaction", err)) } storeTransaction := &StoreTransaction{ @@ -39,38 +39,41 @@ func (storeInstance *Store) Transaction(operation func(*StoreTransaction) error) committed := false defer func() { if !committed { - _ = transaction.Rollback() + if rollbackErr := transaction.Rollback(); rollbackErr != nil { + core.Error("store transaction rollback failed", "err", rollbackErr) + } } }() - if err := operation(storeTransaction); err != nil { - return core.E(opTransaction, "execute transaction", err) + if result := operation(storeTransaction); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opTransaction, "execute transaction", err)) } if err := transaction.Commit(); err != nil { - return core.E(opTransaction, "commit transaction", err) + return core.Fail(core.E(opTransaction, "commit transaction", err)) } committed = true for _, event := range storeTransaction.pendingEvents { storeInstance.notify(event) } - return nil + return core.Ok(nil) } -func (storeTransaction *StoreTransaction) ensureReady(operation string) error { +func (storeTransaction *StoreTransaction) ensureReady(operation string) core.Result { if storeTransaction == nil { - return core.E(operation, "transaction is nil", nil) + return core.Fail(core.E(operation, "transaction is nil", nil)) } if storeTransaction.storeInstance == nil { - return core.E(operation, "transaction store is nil", nil) + return core.Fail(core.E(operation, "transaction store is nil", nil)) } if storeTransaction.sqliteTransaction == nil { - return core.E(operation, "transaction database is nil", nil) + return core.Fail(core.E(operation, "transaction database is nil", nil)) } - if err := storeTransaction.storeInstance.ensureReady(operation); err != nil { - return err + if result := storeTransaction.storeInstance.ensureReady(operation); !result.OK { + return result } - return nil + return core.Ok(nil) } func (storeTransaction *StoreTransaction) recordEvent(event Event) { @@ -82,31 +85,31 @@ func (storeTransaction *StoreTransaction) recordEvent(event Event) { // Usage example: `exists, err := transaction.Exists("config", "colour")` // Usage example: `if exists, _ := transaction.Exists("session", "token"); !exists { return core.E("auth", "session expired", nil) }` -func (storeTransaction *StoreTransaction) Exists(group, key string) (bool, error) { - if err := storeTransaction.ensureReady("store.Transaction.Exists"); err != nil { - return false, err +func (storeTransaction *StoreTransaction) Exists(group, key string) (bool, core.Result) { + if result := storeTransaction.ensureReady("store.Transaction.Exists"); !result.OK { + return false, result } return liveEntryExists(storeTransaction.sqliteTransaction, group, key) } // Usage example: `exists, err := transaction.GroupExists("config")` -func (storeTransaction *StoreTransaction) GroupExists(group string) (bool, error) { - if err := storeTransaction.ensureReady("store.Transaction.GroupExists"); err != nil { - return false, err +func (storeTransaction *StoreTransaction) GroupExists(group string) (bool, core.Result) { + if result := storeTransaction.ensureReady("store.Transaction.GroupExists"); !result.OK { + return false, result } - count, err := storeTransaction.Count(group) - if err != nil { - return false, err + count, result := storeTransaction.Count(group) + if !result.OK { + return false, result } - return count > 0, nil + return count > 0, core.Ok(nil) } // Usage example: `value, err := transaction.Get("config", "colour")` -func (storeTransaction *StoreTransaction) Get(group, key string) (string, error) { - if err := storeTransaction.ensureReady(opTransactionGet); err != nil { - return "", err +func (storeTransaction *StoreTransaction) Get(group, key string) (string, core.Result) { + if result := storeTransaction.ensureReady(opTransactionGet); !result.OK { + return "", result } var value string @@ -116,24 +119,25 @@ func (storeTransaction *StoreTransaction) Get(group, key string) (string, error) group, key, ).Scan(&value, &expiresAt) if err == sql.ErrNoRows { - return "", core.E(opTransactionGet, core.Concat(group, "/", key), NotFoundError) + return "", core.Fail(core.E(opTransactionGet, core.Concat(group, "/", key), NotFoundError)) } if err != nil { - return "", core.E(opTransactionGet, "query row", err) + return "", core.Fail(core.E(opTransactionGet, "query row", err)) } if expiresAt.Valid && expiresAt.Int64 <= time.Now().UnixMilli() { - if err := storeTransaction.Delete(group, key); err != nil { - return "", core.E(opTransactionGet, "delete expired row", err) + if result := storeTransaction.Delete(group, key); !result.OK { + err, _ := result.Value.(error) + return "", core.Fail(core.E(opTransactionGet, "delete expired row", err)) } - return "", core.E(opTransactionGet, core.Concat(group, "/", key), NotFoundError) + return "", core.Fail(core.E(opTransactionGet, core.Concat(group, "/", key), NotFoundError)) } - return value, nil + return value, core.Ok(nil) } // Usage example: `if err := transaction.Set("config", "colour", "blue"); err != nil { return err }` -func (storeTransaction *StoreTransaction) Set(group, key, value string) error { - if err := storeTransaction.ensureReady("store.Transaction.Set"); err != nil { - return err +func (storeTransaction *StoreTransaction) Set(group, key, value string) core.Result { + if result := storeTransaction.ensureReady("store.Transaction.Set"); !result.OK { + return result } _, err := storeTransaction.sqliteTransaction.Exec( @@ -142,16 +146,16 @@ func (storeTransaction *StoreTransaction) Set(group, key, value string) error { group, key, value, ) if err != nil { - return core.E("store.Transaction.Set", "execute upsert", err) + return core.Fail(core.E("store.Transaction.Set", "execute upsert", err)) } storeTransaction.recordEvent(Event{Type: EventSet, Group: group, Key: key, Value: value, Timestamp: time.Now()}) - return nil + return core.Ok(nil) } // Usage example: `if err := transaction.SetWithTTL("session", "token", "abc123", time.Minute); err != nil { return err }` -func (storeTransaction *StoreTransaction) SetWithTTL(group, key, value string, timeToLive time.Duration) error { - if err := storeTransaction.ensureReady("store.Transaction.SetWithTTL"); err != nil { - return err +func (storeTransaction *StoreTransaction) SetWithTTL(group, key, value string, timeToLive time.Duration) core.Result { + if result := storeTransaction.ensureReady("store.Transaction.SetWithTTL"); !result.OK { + return result } expiresAt := time.Now().Add(timeToLive).UnixMilli() @@ -161,16 +165,16 @@ func (storeTransaction *StoreTransaction) SetWithTTL(group, key, value string, t group, key, value, expiresAt, ) if err != nil { - return core.E("store.Transaction.SetWithTTL", "execute upsert with expiry", err) + return core.Fail(core.E("store.Transaction.SetWithTTL", "execute upsert with expiry", err)) } storeTransaction.recordEvent(Event{Type: EventSet, Group: group, Key: key, Value: value, Timestamp: time.Now()}) - return nil + return core.Ok(nil) } // Usage example: `if err := transaction.Delete("config", "colour"); err != nil { return err }` -func (storeTransaction *StoreTransaction) Delete(group, key string) error { - if err := storeTransaction.ensureReady(opTransactionDelete); err != nil { - return err +func (storeTransaction *StoreTransaction) Delete(group, key string) core.Result { + if result := storeTransaction.ensureReady(opTransactionDelete); !result.OK { + return result } deleteResult, err := storeTransaction.sqliteTransaction.Exec( @@ -178,22 +182,22 @@ func (storeTransaction *StoreTransaction) Delete(group, key string) error { group, key, ) if err != nil { - return core.E(opTransactionDelete, "delete row", err) + return core.Fail(core.E(opTransactionDelete, "delete row", err)) } deletedRows, rowsAffectedError := deleteResult.RowsAffected() if rowsAffectedError != nil { - return core.E(opTransactionDelete, "count deleted rows", rowsAffectedError) + return core.Fail(core.E(opTransactionDelete, "count deleted rows", rowsAffectedError)) } if deletedRows > 0 { storeTransaction.recordEvent(Event{Type: EventDelete, Group: group, Key: key, Timestamp: time.Now()}) } - return nil + return core.Ok(nil) } // Usage example: `if err := transaction.DeleteGroup("cache"); err != nil { return err }` -func (storeTransaction *StoreTransaction) DeleteGroup(group string) error { - if err := storeTransaction.ensureReady(opTransactionDeleteGroup); err != nil { - return err +func (storeTransaction *StoreTransaction) DeleteGroup(group string) core.Result { + if result := storeTransaction.ensureReady(opTransactionDeleteGroup); !result.OK { + return result } deleteResult, err := storeTransaction.sqliteTransaction.Exec( @@ -201,22 +205,22 @@ func (storeTransaction *StoreTransaction) DeleteGroup(group string) error { group, ) if err != nil { - return core.E(opTransactionDeleteGroup, "delete group", err) + return core.Fail(core.E(opTransactionDeleteGroup, "delete group", err)) } deletedRows, rowsAffectedError := deleteResult.RowsAffected() if rowsAffectedError != nil { - return core.E(opTransactionDeleteGroup, "count deleted rows", rowsAffectedError) + return core.Fail(core.E(opTransactionDeleteGroup, "count deleted rows", rowsAffectedError)) } if deletedRows > 0 { storeTransaction.recordEvent(Event{Type: EventDeleteGroup, Group: group, Timestamp: time.Now()}) } - return nil + return core.Ok(nil) } // Usage example: `if err := transaction.DeletePrefix("tenant-a:"); err != nil { return err }` -func (storeTransaction *StoreTransaction) DeletePrefix(groupPrefix string) error { - if err := storeTransaction.ensureReady(opTransactionDeletePrefix); err != nil { - return err +func (storeTransaction *StoreTransaction) DeletePrefix(groupPrefix string) core.Result { + if result := storeTransaction.ensureReady(opTransactionDeletePrefix); !result.OK { + return result } var rows *sql.Rows @@ -232,7 +236,7 @@ func (storeTransaction *StoreTransaction) DeletePrefix(groupPrefix string) error ) } if err != nil { - return core.E(opTransactionDeletePrefix, "list groups", err) + return core.Fail(core.E(opTransactionDeletePrefix, "list groups", err)) } defer func() { _ = rows.Close() }() @@ -240,25 +244,26 @@ func (storeTransaction *StoreTransaction) DeletePrefix(groupPrefix string) error for rows.Next() { var groupName string if err := rows.Scan(&groupName); err != nil { - return core.E(opTransactionDeletePrefix, "scan group name", err) + return core.Fail(core.E(opTransactionDeletePrefix, "scan group name", err)) } groupNames = append(groupNames, groupName) } if err := rows.Err(); err != nil { - return core.E(opTransactionDeletePrefix, "iterate groups", err) + return core.Fail(core.E(opTransactionDeletePrefix, "iterate groups", err)) } for _, groupName := range groupNames { - if err := storeTransaction.DeleteGroup(groupName); err != nil { - return core.E(opTransactionDeletePrefix, "delete group", err) + if result := storeTransaction.DeleteGroup(groupName); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opTransactionDeletePrefix, "delete group", err)) } } - return nil + return core.Ok(nil) } // Usage example: `keyCount, err := transaction.Count("config")` -func (storeTransaction *StoreTransaction) Count(group string) (int, error) { - if err := storeTransaction.ensureReady("store.Transaction.Count"); err != nil { - return 0, err +func (storeTransaction *StoreTransaction) Count(group string) (int, core.Result) { + if result := storeTransaction.ensureReady("store.Transaction.Count"); !result.OK { + return 0, result } var count int @@ -267,37 +272,37 @@ func (storeTransaction *StoreTransaction) Count(group string) (int, error) { group, time.Now().UnixMilli(), ).Scan(&count) if err != nil { - return 0, core.E("store.Transaction.Count", "count rows", err) + return 0, core.Fail(core.E("store.Transaction.Count", "count rows", err)) } - return count, nil + return count, core.Ok(nil) } // Usage example: `colourEntries, err := transaction.GetAll("config")` -func (storeTransaction *StoreTransaction) GetAll(group string) (map[string]string, error) { - if err := storeTransaction.ensureReady("store.Transaction.GetAll"); err != nil { - return nil, err +func (storeTransaction *StoreTransaction) GetAll(group string) (map[string]string, core.Result) { + if result := storeTransaction.ensureReady("store.Transaction.GetAll"); !result.OK { + return nil, result } entriesByKey := make(map[string]string) for entry, err := range storeTransaction.All(group) { if err != nil { - return nil, core.E("store.Transaction.GetAll", "iterate rows", err) + return nil, core.Fail(core.E("store.Transaction.GetAll", "iterate rows", err)) } entriesByKey[entry.Key] = entry.Value } - return entriesByKey, nil + return entriesByKey, core.Ok(nil) } // Usage example: `page, err := transaction.GetPage("config", 0, 25); if err != nil { return }; for _, entry := range page { fmt.Println(entry.Key, entry.Value) }` -func (storeTransaction *StoreTransaction) GetPage(group string, offset, limit int) ([]KeyValue, error) { - if err := storeTransaction.ensureReady(opTransactionGetPage); err != nil { - return nil, err +func (storeTransaction *StoreTransaction) GetPage(group string, offset, limit int) ([]KeyValue, core.Result) { + if result := storeTransaction.ensureReady(opTransactionGetPage); !result.OK { + return nil, result } if offset < 0 { - return nil, core.E(opTransactionGetPage, "offset must be zero or positive", nil) + return nil, core.Fail(core.E(opTransactionGetPage, "offset must be zero or positive", nil)) } if limit < 0 { - return nil, core.E(opTransactionGetPage, "limit must be zero or positive", nil) + return nil, core.Fail(core.E(opTransactionGetPage, "limit must be zero or positive", nil)) } rows, err := storeTransaction.sqliteTransaction.Query( @@ -305,7 +310,7 @@ func (storeTransaction *StoreTransaction) GetPage(group string, offset, limit in group, time.Now().UnixMilli(), limit, offset, ) if err != nil { - return nil, core.E(opTransactionGetPage, "query rows", err) + return nil, core.Fail(core.E(opTransactionGetPage, "query rows", err)) } defer func() { _ = rows.Close() }() @@ -313,14 +318,14 @@ func (storeTransaction *StoreTransaction) GetPage(group string, offset, limit in for rows.Next() { var entry KeyValue if err := rows.Scan(&entry.Key, &entry.Value); err != nil { - return nil, core.E(opTransactionGetPage, "scan row", err) + return nil, core.Fail(core.E(opTransactionGetPage, "scan row", err)) } page = append(page, entry) } if err := rows.Err(); err != nil { - return nil, core.E(opTransactionGetPage, rowsIterationMessage, err) + return nil, core.Fail(core.E(opTransactionGetPage, rowsIterationMessage, err)) } - return page, nil + return page, core.Ok(nil) } // Usage example: `for entry, err := range transaction.All("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }` @@ -331,7 +336,8 @@ func (storeTransaction *StoreTransaction) All(group string) iter.Seq2[KeyValue, // Usage example: `for entry, err := range transaction.AllSeq("config") { if err != nil { break }; fmt.Println(entry.Key, entry.Value) }` func (storeTransaction *StoreTransaction) AllSeq(group string) iter.Seq2[KeyValue, error] { return func(yield func(KeyValue, error) bool) { - if err := storeTransaction.ensureReady(opTransactionAll); err != nil { + if result := storeTransaction.ensureReady(opTransactionAll); !result.OK { + err, _ := result.Value.(error) yield(KeyValue{}, err) return } @@ -351,9 +357,9 @@ func (storeTransaction *StoreTransaction) AllSeq(group string) iter.Seq2[KeyValu } // Usage example: `removedRows, err := transaction.CountAll("tenant-a:")` -func (storeTransaction *StoreTransaction) CountAll(groupPrefix string) (int, error) { - if err := storeTransaction.ensureReady("store.Transaction.CountAll"); err != nil { - return 0, err +func (storeTransaction *StoreTransaction) CountAll(groupPrefix string) (int, core.Result) { + if result := storeTransaction.ensureReady("store.Transaction.CountAll"); !result.OK { + return 0, result } var count int @@ -370,26 +376,26 @@ func (storeTransaction *StoreTransaction) CountAll(groupPrefix string) (int, err ).Scan(&count) } if err != nil { - return 0, core.E("store.Transaction.CountAll", "count rows", err) + return 0, core.Fail(core.E("store.Transaction.CountAll", "count rows", err)) } - return count, nil + return count, core.Ok(nil) } // Usage example: `groupNames, err := transaction.Groups("tenant-a:")` // Usage example: `groupNames, err := transaction.Groups()` -func (storeTransaction *StoreTransaction) Groups(groupPrefix ...string) ([]string, error) { - if err := storeTransaction.ensureReady("store.Transaction.Groups"); err != nil { - return nil, err +func (storeTransaction *StoreTransaction) Groups(groupPrefix ...string) ([]string, core.Result) { + if result := storeTransaction.ensureReady("store.Transaction.Groups"); !result.OK { + return nil, result } var groupNames []string for groupName, err := range storeTransaction.GroupsSeq(groupPrefix...) { if err != nil { - return nil, err + return nil, core.Fail(err) } groupNames = append(groupNames, groupName) } - return groupNames, nil + return groupNames, core.Ok(nil) } // Usage example: `for groupName, err := range transaction.GroupsSeq("tenant-a:") { if err != nil { break }; fmt.Println(groupName) }` @@ -397,7 +403,8 @@ func (storeTransaction *StoreTransaction) Groups(groupPrefix ...string) ([]strin func (storeTransaction *StoreTransaction) GroupsSeq(groupPrefix ...string) iter.Seq2[string, error] { actualGroupPrefix := firstStringOrEmpty(groupPrefix) return func(yield func(string, error) bool) { - if err := storeTransaction.ensureReady(opTransactionGroupsSeq); err != nil { + if result := storeTransaction.ensureReady(opTransactionGroupsSeq); !result.OK { + err, _ := result.Value.(error) yield("", err) return } @@ -427,69 +434,70 @@ func (storeTransaction *StoreTransaction) GroupsSeq(groupPrefix ...string) iter. } // Usage example: `renderedTemplate, err := transaction.Render("Hello {{ .name }}", "user")` -func (storeTransaction *StoreTransaction) Render(templateSource, group string) (string, error) { - if err := storeTransaction.ensureReady(opTransactionRender); err != nil { - return "", err +func (storeTransaction *StoreTransaction) Render(templateSource, group string) (string, core.Result) { + if result := storeTransaction.ensureReady(opTransactionRender); !result.OK { + return "", result } templateData := make(map[string]string) for entry, err := range storeTransaction.All(group) { if err != nil { - return "", core.E(opTransactionRender, "iterate rows", err) + return "", core.Fail(core.E(opTransactionRender, "iterate rows", err)) } templateData[entry.Key] = entry.Value } renderTemplate, err := template.New("render").Parse(templateSource) if err != nil { - return "", core.E(opTransactionRender, "parse template", err) + return "", core.Fail(core.E(opTransactionRender, "parse template", err)) } builder := core.NewBuilder() if err := renderTemplate.Execute(builder, templateData); err != nil { - return "", core.E(opTransactionRender, "execute template", err) + return "", core.Fail(core.E(opTransactionRender, "execute template", err)) } - return builder.String(), nil + return builder.String(), core.Ok(nil) } // Usage example: `parts, err := transaction.GetSplit("config", "hosts", ","); if err != nil { return }; for part := range parts { fmt.Println(part) }` -func (storeTransaction *StoreTransaction) GetSplit(group, key, separator string) (iter.Seq[string], error) { - if err := storeTransaction.ensureReady("store.Transaction.GetSplit"); err != nil { - return nil, err +func (storeTransaction *StoreTransaction) GetSplit(group, key, separator string) (iter.Seq[string], core.Result) { + if result := storeTransaction.ensureReady("store.Transaction.GetSplit"); !result.OK { + return nil, result } - value, err := storeTransaction.Get(group, key) - if err != nil { - return nil, err + value, result := storeTransaction.Get(group, key) + if !result.OK { + return nil, result } - return splitValueSeq(value, separator), nil + return splitValueSeq(value, separator), core.Ok(nil) } // Usage example: `fields, err := transaction.GetFields("config", "flags"); if err != nil { return }; for field := range fields { fmt.Println(field) }` -func (storeTransaction *StoreTransaction) GetFields(group, key string) (iter.Seq[string], error) { - if err := storeTransaction.ensureReady("store.Transaction.GetFields"); err != nil { - return nil, err +func (storeTransaction *StoreTransaction) GetFields(group, key string) (iter.Seq[string], core.Result) { + if result := storeTransaction.ensureReady("store.Transaction.GetFields"); !result.OK { + return nil, result } - value, err := storeTransaction.Get(group, key) - if err != nil { - return nil, err + value, result := storeTransaction.Get(group, key) + if !result.OK { + return nil, result } - return fieldsValueSeq(value), nil + return fieldsValueSeq(value), core.Ok(nil) } // Usage example: `removedRows, err := transaction.PurgeExpired(); if err != nil { return err }; fmt.Println(removedRows)` -func (storeTransaction *StoreTransaction) PurgeExpired() (int64, error) { - if err := storeTransaction.ensureReady("store.Transaction.PurgeExpired"); err != nil { - return 0, err +func (storeTransaction *StoreTransaction) PurgeExpired() (int64, core.Result) { + if result := storeTransaction.ensureReady("store.Transaction.PurgeExpired"); !result.OK { + return 0, result } cutoffUnixMilli := time.Now().UnixMilli() - expiredEntries, err := deleteExpiredEntriesMatchingGroupPrefix(storeTransaction.sqliteTransaction, "", cutoffUnixMilli) - if err != nil { - return 0, core.E("store.Transaction.PurgeExpired", "delete expired rows", err) + expiredEntries, result := deleteExpiredEntriesMatchingGroupPrefix(storeTransaction.sqliteTransaction, "", cutoffUnixMilli) + if !result.OK { + err, _ := result.Value.(error) + return 0, core.Fail(core.E("store.Transaction.PurgeExpired", "delete expired rows", err)) } storeTransaction.recordExpiredEntries(expiredEntries) - return int64(len(expiredEntries)), nil + return int64(len(expiredEntries)), core.Ok(nil) } func (storeTransaction *StoreTransaction) recordExpiredEntries(expiredEntries []expiredEntryRef) { diff --git a/go/transaction_example_test.go b/go/transaction_example_test.go new file mode 100644 index 0000000..0071c0c --- /dev/null +++ b/go/transaction_example_test.go @@ -0,0 +1,256 @@ +package store + +import ( + "time" + + core "dappco.re/go" +) + +func ExampleStore_Transaction() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + return transaction.Set("config", "colour", "blue") + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_Exists() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "colour", "blue")) + exists, existsResult := transaction.Exists("config", "colour") + exampleRequireOK(existsResult) + core.Println(exists) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_GroupExists() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "colour", "blue")) + exists, existsResult := transaction.GroupExists("config") + exampleRequireOK(existsResult) + core.Println(exists) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_Get() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "colour", "blue")) + value, getResult := transaction.Get("config", "colour") + exampleRequireOK(getResult) + core.Println(value) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_Set() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + return transaction.Set("config", "colour", "blue") + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_SetWithTTL() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + return transaction.SetWithTTL("cache", "token", "abc", time.Minute) + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_Delete() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "colour", "blue")) + return transaction.Delete("config", "colour") + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_DeleteGroup() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "colour", "blue")) + return transaction.DeleteGroup("config") + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_DeletePrefix() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "colour", "blue")) + return transaction.DeletePrefix("conf") + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_Count() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "colour", "blue")) + count, countResult := transaction.Count("config") + exampleRequireOK(countResult) + core.Println(count) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_GetAll() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "colour", "blue")) + values, valuesResult := transaction.GetAll("config") + exampleRequireOK(valuesResult) + core.Println(values["colour"]) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_GetPage() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "colour", "blue")) + entries, entriesResult := transaction.GetPage("config", 0, 10) + exampleRequireOK(entriesResult) + core.Println(len(entries)) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_All() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "colour", "blue")) + entries := exampleCollectKeyValues(transaction.All("config")) + core.Println(len(entries)) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_AllSeq() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "colour", "blue")) + entries := exampleCollectKeyValues(transaction.AllSeq("config")) + core.Println(len(entries)) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_CountAll() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "colour", "blue")) + count, countResult := transaction.CountAll("conf") + exampleRequireOK(countResult) + core.Println(count) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_Groups() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "colour", "blue")) + groups, groupsResult := transaction.Groups() + exampleRequireOK(groupsResult) + core.Println(groups) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_GroupsSeq() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "colour", "blue")) + groups := exampleCollectGroups(transaction.GroupsSeq()) + core.Println(groups) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_Render() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "colour", "blue")) + rendered, renderResult := transaction.Render("colour={{.colour}}", "config") + exampleRequireOK(renderResult) + core.Println(rendered) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_GetSplit() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "tags", "alpha,beta")) + seq, splitResult := transaction.GetSplit("config", "tags", ",") + exampleRequireOK(splitResult) + core.Println(exampleCollectStrings(seq)) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_GetFields() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.Set("config", "text", "alpha beta")) + seq, fieldsResult := transaction.GetFields("config", "text") + exampleRequireOK(fieldsResult) + core.Println(exampleCollectStrings(seq)) + return core.Ok(nil) + }) + exampleRequireOK(result) +} + +func ExampleStoreTransaction_PurgeExpired() { + storeInstance := exampleOpenStore() + defer exampleCloseStore(storeInstance) + result := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + exampleRequireOK(transaction.SetWithTTL("cache", "token", "abc", time.Nanosecond)) + removed, purgeResult := transaction.PurgeExpired() + exampleRequireOK(purgeResult) + core.Println(removed) + return core.Ok(nil) + }) + exampleRequireOK(result) +} diff --git a/transaction_test.go b/go/transaction_test.go similarity index 79% rename from transaction_test.go rename to go/transaction_test.go index 5b87e23..e23f111 100644 --- a/transaction_test.go +++ b/go/transaction_test.go @@ -15,14 +15,14 @@ func TestTransaction_Transaction_Good_CommitsMultipleWrites(t *testing.T) { events := storeInstance.Watch("*") defer storeInstance.Unwatch("*", events) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { - if err := transaction.Set("alpha", "first", "1"); err != nil { + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + if err := transaction.Set("alpha", "first", "1"); !err.OK { return err } - if err := transaction.Set("beta", "second", "2"); err != nil { + if err := transaction.Set("beta", "second", "2"); !err.OK { return err } - return nil + return core.Ok(nil) }) assertNoError(t, err) @@ -48,11 +48,11 @@ func TestTransaction_Transaction_Good_RollbackOnError(t *testing.T) { storeInstance, _ := New(testMemoryDatabasePath) defer func() { _ = storeInstance.Close() }() - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { - if err := transaction.Set("alpha", "first", "1"); err != nil { + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + if err := transaction.Set("alpha", "first", "1"); !err.OK { return err } - return core.E("test", "force rollback", nil) + return core.Fail(core.E("test", "force rollback", nil)) }) assertError(t, err) @@ -67,11 +67,11 @@ func TestTransaction_Transaction_Good_DeletesAtomically(t *testing.T) { assertNoError(t, storeInstance.Set("alpha", "first", "1")) assertNoError(t, storeInstance.Set("beta", "second", "2")) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { - if err := transaction.DeletePrefix(""); err != nil { + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + if err := transaction.DeletePrefix(""); !err.OK { return err } - return nil + return core.Ok(nil) }) assertNoError(t, err) @@ -85,14 +85,14 @@ func TestTransaction_Transaction_Good_ReadHelpersSeePendingWrites(t *testing.T) storeInstance, _ := New(testMemoryDatabasePath) defer func() { _ = storeInstance.Close() }() - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { - if err := transaction.Set("config", "colour", "blue"); err != nil { + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + if err := transaction.Set("config", "colour", "blue"); !err.OK { return err } - if err := transaction.Set("config", "hosts", "alpha beta"); err != nil { + if err := transaction.Set("config", "hosts", "alpha beta"); !err.OK { return err } - if err := transaction.Set("audit", "enabled", "true"); err != nil { + if err := transaction.Set("audit", "enabled", "true"); !err.OK { return err } @@ -120,7 +120,7 @@ func TestTransaction_Transaction_Good_ReadHelpersSeePendingWrites(t *testing.T) assertNoError(t, err) assertEqual(t, []string{"alpha", "beta"}, collectSeq(t, fieldParts)) - return nil + return core.Ok(nil) }) assertNoError(t, err) } @@ -132,11 +132,11 @@ func TestTransaction_Transaction_Good_PurgeExpired(t *testing.T) { assertNoError(t, storeInstance.SetWithTTL("alpha", "ephemeral", "gone", 1*time.Millisecond)) time.Sleep(5 * time.Millisecond) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { removedRows, err := transaction.PurgeExpired() assertNoError(t, err) assertEqual(t, int64(1), removedRows) - return nil + return core.Ok(nil) }) assertNoError(t, err) @@ -150,7 +150,7 @@ func TestTransaction_Transaction_Good_Exists(t *testing.T) { assertNoError(t, storeInstance.Set("config", "colour", "blue")) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { exists, err := transaction.Exists("config", "colour") assertNoError(t, err) assertTrue(t, exists) @@ -159,7 +159,7 @@ func TestTransaction_Transaction_Good_Exists(t *testing.T) { assertNoError(t, err) assertFalse(t, exists) - return nil + return core.Ok(nil) }) assertNoError(t, err) } @@ -168,12 +168,12 @@ func TestTransaction_Transaction_Good_ExistsSeesPendingWrites(t *testing.T) { storeInstance, _ := New(testMemoryDatabasePath) defer func() { _ = storeInstance.Close() }() - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { exists, err := transaction.Exists("config", "colour") assertNoError(t, err) assertFalse(t, exists) - if err := transaction.Set("config", "colour", "blue"); err != nil { + if err := transaction.Set("config", "colour", "blue"); !err.OK { return err } @@ -181,7 +181,7 @@ func TestTransaction_Transaction_Good_ExistsSeesPendingWrites(t *testing.T) { assertNoError(t, err) assertTrue(t, exists) - return nil + return core.Ok(nil) }) assertNoError(t, err) } @@ -190,12 +190,12 @@ func TestTransaction_Transaction_Good_GroupExists(t *testing.T) { storeInstance, _ := New(testMemoryDatabasePath) defer func() { _ = storeInstance.Close() }() - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { exists, err := transaction.GroupExists("config") assertNoError(t, err) assertFalse(t, exists) - if err := transaction.Set("config", "colour", "blue"); err != nil { + if err := transaction.Set("config", "colour", "blue"); !err.OK { return err } @@ -203,7 +203,7 @@ func TestTransaction_Transaction_Good_GroupExists(t *testing.T) { assertNoError(t, err) assertTrue(t, exists) - return nil + return core.Ok(nil) }) assertNoError(t, err) } @@ -214,12 +214,12 @@ func TestTransaction_ScopedStoreTransaction_Good_ExistsAndGroupExists(t *testing scopedStore := NewScoped(storeInstance, testTenantA) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { exists, err := transaction.Exists("colour") assertNoError(t, err) assertFalse(t, exists) - if err := transaction.Set("colour", "blue"); err != nil { + if err := transaction.Set("colour", "blue"); !err.OK { return err } @@ -231,7 +231,7 @@ func TestTransaction_ScopedStoreTransaction_Good_ExistsAndGroupExists(t *testing assertNoError(t, err) assertFalse(t, exists) - if err := transaction.SetIn("config", "theme", "dark"); err != nil { + if err := transaction.SetIn("config", "theme", "dark"); !err.OK { return err } @@ -243,7 +243,7 @@ func TestTransaction_ScopedStoreTransaction_Good_ExistsAndGroupExists(t *testing assertNoError(t, err) assertFalse(t, groupExists) - return nil + return core.Ok(nil) }) assertNoError(t, err) } @@ -254,14 +254,14 @@ func TestTransaction_ScopedStoreTransaction_Good_GetPage(t *testing.T) { scopedStore := NewScoped(storeInstance, testTenantA) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { - if err := transaction.SetIn("items", "charlie", "3"); err != nil { + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + if err := transaction.SetIn("items", "charlie", "3"); !err.OK { return err } - if err := transaction.SetIn("items", "alpha", "1"); err != nil { + if err := transaction.SetIn("items", "alpha", "1"); !err.OK { return err } - if err := transaction.SetIn("items", "bravo", "2"); err != nil { + if err := transaction.SetIn("items", "bravo", "2"); !err.OK { return err } @@ -269,7 +269,7 @@ func TestTransaction_ScopedStoreTransaction_Good_GetPage(t *testing.T) { assertNoError(t, err) assertLen(t, page, 1) assertEqual(t, KeyValue{Key: "bravo", Value: "2"}, page[0]) - return nil + return core.Ok(nil) }) assertNoError(t, err) } @@ -284,11 +284,11 @@ func TestTransaction_ScopedStoreTransaction_Good_CommitsNamespacedWrites(t *test }) assertNoError(t, err) - err = scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { - if err := transaction.Set("theme", "dark"); err != nil { + err = scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { + if err := transaction.Set("theme", "dark"); !err.OK { return err } - if err := transaction.SetIn("preferences", "locale", "en-GB"); err != nil { + if err := transaction.SetIn("preferences", "locale", "en-GB"); !err.OK { return err } @@ -304,7 +304,7 @@ func TestTransaction_ScopedStoreTransaction_Good_CommitsNamespacedWrites(t *test assertNoError(t, err) assertEqual(t, []string{"default", "preferences"}, groupNames) - return nil + return core.Ok(nil) }) assertNoError(t, err) @@ -326,11 +326,11 @@ func TestTransaction_ScopedStoreTransaction_Good_PurgeExpired(t *testing.T) { assertNoError(t, scopedStore.SetWithTTL("session", "token", "abc123", 1*time.Millisecond)) time.Sleep(5 * time.Millisecond) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { removedRows, err := transaction.PurgeExpired() assertNoError(t, err) assertEqual(t, int64(1), removedRows) - return nil + return core.Ok(nil) }) assertNoError(t, err) @@ -348,20 +348,20 @@ func TestTransaction_ScopedStoreTransaction_Good_QuotaUsesPendingWrites(t *testi }) assertNoError(t, err) - err = scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + err = scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { assertNoError(t, transaction.SetIn("group-1", "first", "1")) assertNoError(t, transaction.SetIn("group-2", "second", "2")) err := transaction.SetIn("group-2", "third", "3") assertError(t, err) - assertTrue(t, core.Is(err, QuotaExceededError)) + assertTrue(t, errIs(errorValue(err), QuotaExceededError)) return err }) assertError(t, err) - assertTrue(t, core.Is(err, QuotaExceededError)) + assertTrue(t, errIs(errorValue(err), QuotaExceededError)) _, getErr := storeInstance.Get("tenant-a:group-1", "first") - assertTrue(t, core.Is(getErr, NotFoundError)) + assertTrue(t, errIs(errorValue(getErr), NotFoundError)) } func TestTransaction_ScopedStoreTransaction_Good_DeletePrefix(t *testing.T) { @@ -376,15 +376,15 @@ func TestTransaction_ScopedStoreTransaction_Good_DeletePrefix(t *testing.T) { assertNoError(t, scopedStore.SetIn("config", "colour", "blue")) assertNoError(t, otherScopedStore.SetIn("cache", "theme", "keep")) - err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error { + err := scopedStore.Transaction(func(transaction *ScopedStoreTransaction) core.Result { return transaction.DeletePrefix("cache") }) assertNoError(t, err) _, getErr := scopedStore.GetFrom("cache", "theme") - assertTrue(t, core.Is(getErr, NotFoundError)) + assertTrue(t, errIs(errorValue(getErr), NotFoundError)) _, getErr = scopedStore.GetFrom("cache-warm", "status") - assertTrue(t, core.Is(getErr, NotFoundError)) + assertTrue(t, errIs(errorValue(getErr), NotFoundError)) colourValue, getErr := scopedStore.GetFrom("config", "colour") assertNoError(t, getErr) @@ -406,35 +406,35 @@ func collectSeq[T any](t *testing.T, sequence iter.Seq[T]) []T { } func TestTransaction_Store_Transaction_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { return transaction.Set("config", "colour", "blue") }) + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { return transaction.Set("config", "colour", "blue") }) AssertNoError(t, err) - AssertEqual(t, "blue", ax7MustGet(t, storeInstance, "config", "colour")) + AssertEqual(t, "blue", fixtureMustGet(t, storeInstance, "config", "colour")) } func TestTransaction_Store_Transaction_Bad(t *T) { - storeInstance := ax7Store(t) + storeInstance := fixtureStore(t) err := storeInstance.Transaction(nil) AssertError(t, err) - AssertFalse(t, ax7MustExists(t, storeInstance, "config", "colour")) + AssertFalse(t, fixtureMustExists(t, storeInstance, "config", "colour")) } func TestTransaction_Store_Transaction_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { return NewError("rollback") }) + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { return core.Fail(NewError("rollback")) }) AssertError(t, err) - AssertFalse(t, ax7MustExists(t, storeInstance, "config", "colour")) + AssertFalse(t, fixtureMustExists(t, storeInstance, "config", "colour")) } func TestTransaction_StoreTransaction_Set_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { err := transaction.Set("config", "colour", "blue") AssertNoError(t, err) - return nil + return core.Ok(nil) }) AssertNoError(t, err) - AssertEqual(t, "blue", ax7MustGet(t, storeInstance, "config", "colour")) + AssertEqual(t, "blue", fixtureMustGet(t, storeInstance, "config", "colour")) } func TestTransaction_StoreTransaction_Set_Bad(t *T) { @@ -444,28 +444,28 @@ func TestTransaction_StoreTransaction_Set_Bad(t *T) { } func TestTransaction_StoreTransaction_Set_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "colour", "blue")) err := transaction.Set("config", "colour", "green") AssertNoError(t, err) got, err := transaction.Get("config", "colour") AssertNoError(t, err) AssertEqual(t, "green", got) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_SetWithTTL_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { err := transaction.SetWithTTL("session", "token", "abc", Hour) AssertNoError(t, err) - return nil + return core.Ok(nil) }) AssertNoError(t, err) - AssertTrue(t, ax7MustExists(t, storeInstance, "session", "token")) + AssertTrue(t, fixtureMustExists(t, storeInstance, "session", "token")) } func TestTransaction_StoreTransaction_SetWithTTL_Bad(t *T) { @@ -475,25 +475,25 @@ func TestTransaction_StoreTransaction_SetWithTTL_Bad(t *T) { } func TestTransaction_StoreTransaction_SetWithTTL_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.SetWithTTL("session", "token", "abc", -Millisecond)) exists, err := transaction.Exists("session", "token") AssertNoError(t, err) AssertFalse(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_Get_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "colour", "blue")) got, err := transaction.Get("config", "colour") AssertNoError(t, err) AssertEqual(t, "blue", got) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -506,24 +506,24 @@ func TestTransaction_StoreTransaction_Get_Bad(t *T) { } func TestTransaction_StoreTransaction_Get_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { got, err := transaction.Get("", "") AssertError(t, err) AssertEqual(t, "", got) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_Exists_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "colour", "blue")) exists, err := transaction.Exists("config", "colour") AssertNoError(t, err) AssertTrue(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -536,24 +536,24 @@ func TestTransaction_StoreTransaction_Exists_Bad(t *T) { } func TestTransaction_StoreTransaction_Exists_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { exists, err := transaction.Exists("", "") AssertNoError(t, err) AssertFalse(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_GroupExists_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "colour", "blue")) exists, err := transaction.GroupExists("config") AssertNoError(t, err) AssertTrue(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -566,26 +566,26 @@ func TestTransaction_StoreTransaction_GroupExists_Bad(t *T) { } func TestTransaction_StoreTransaction_GroupExists_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { exists, err := transaction.GroupExists("") AssertNoError(t, err) AssertFalse(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_Delete_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "colour", "blue")) err := transaction.Delete("config", "colour") AssertNoError(t, err) - return nil + return core.Ok(nil) }) AssertNoError(t, err) - AssertFalse(t, ax7MustExists(t, storeInstance, "config", "colour")) + AssertFalse(t, fixtureMustExists(t, storeInstance, "config", "colour")) } func TestTransaction_StoreTransaction_Delete_Bad(t *T) { @@ -595,28 +595,28 @@ func TestTransaction_StoreTransaction_Delete_Bad(t *T) { } func TestTransaction_StoreTransaction_Delete_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { err := transaction.Delete("missing", "key") AssertNoError(t, err) exists, err := transaction.Exists("missing", "key") AssertNoError(t, err) AssertFalse(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_DeleteGroup_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "colour", "blue")) err := transaction.DeleteGroup("config") AssertNoError(t, err) - return nil + return core.Ok(nil) }) AssertNoError(t, err) - AssertFalse(t, ax7MustGroupExists(t, storeInstance, "config")) + AssertFalse(t, fixtureMustGroupExists(t, storeInstance, "config")) } func TestTransaction_StoreTransaction_DeleteGroup_Bad(t *T) { @@ -626,28 +626,28 @@ func TestTransaction_StoreTransaction_DeleteGroup_Bad(t *T) { } func TestTransaction_StoreTransaction_DeleteGroup_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { err := transaction.DeleteGroup("missing") AssertNoError(t, err) exists, err := transaction.GroupExists("missing") AssertNoError(t, err) AssertFalse(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_DeletePrefix_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set(testTenantAConfigGroup, "colour", "blue")) err := transaction.DeletePrefix(testTenantAPrefix) AssertNoError(t, err) - return nil + return core.Ok(nil) }) AssertNoError(t, err) - AssertFalse(t, ax7MustGroupExists(t, storeInstance, testTenantAConfigGroup)) + AssertFalse(t, fixtureMustGroupExists(t, storeInstance, testTenantAConfigGroup)) } func TestTransaction_StoreTransaction_DeletePrefix_Bad(t *T) { @@ -657,26 +657,26 @@ func TestTransaction_StoreTransaction_DeletePrefix_Bad(t *T) { } func TestTransaction_StoreTransaction_DeletePrefix_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { err := transaction.DeletePrefix("missing") AssertNoError(t, err) exists, err := transaction.GroupExists("missing") AssertNoError(t, err) AssertFalse(t, exists) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_Count_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "a", "1")) count, err := transaction.Count("config") AssertNoError(t, err) AssertEqual(t, 1, count) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -689,24 +689,24 @@ func TestTransaction_StoreTransaction_Count_Bad(t *T) { } func TestTransaction_StoreTransaction_Count_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { count, err := transaction.Count("") AssertNoError(t, err) AssertEqual(t, 0, count) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_GetAll_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "a", "1")) entries, err := transaction.GetAll("config") AssertNoError(t, err) AssertEqual(t, "1", entries["a"]) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -719,24 +719,24 @@ func TestTransaction_StoreTransaction_GetAll_Bad(t *T) { } func TestTransaction_StoreTransaction_GetAll_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { entries, err := transaction.GetAll("") AssertNoError(t, err) AssertEmpty(t, entries) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_GetPage_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "a", "1")) page, err := transaction.GetPage("config", 0, 1) AssertNoError(t, err) AssertEqual(t, "a", page[0].Key) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -749,84 +749,84 @@ func TestTransaction_StoreTransaction_GetPage_Bad(t *T) { } func TestTransaction_StoreTransaction_GetPage_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { page, err := transaction.GetPage("missing", 0, 1) AssertNoError(t, err) AssertEmpty(t, page) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_All_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "a", "1")) - entries, err := ax7CollectKeyValues(transaction.All("config")) + entries, err := fixtureCollectKeyValues(transaction.All("config")) AssertNoError(t, err) AssertEqual(t, "a", entries[0].Key) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_All_Bad(t *T) { var transaction *StoreTransaction - entries, err := ax7CollectKeyValues(transaction.All("missing")) + entries, err := fixtureCollectKeyValues(transaction.All("missing")) AssertError(t, err) AssertEmpty(t, entries) } func TestTransaction_StoreTransaction_All_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { - entries, err := ax7CollectKeyValues(transaction.All("")) + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + entries, err := fixtureCollectKeyValues(transaction.All("")) AssertNoError(t, err) AssertEmpty(t, entries) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_AllSeq_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "a", "1")) - entries, err := ax7CollectKeyValues(transaction.AllSeq("config")) + entries, err := fixtureCollectKeyValues(transaction.AllSeq("config")) AssertNoError(t, err) AssertEqual(t, "1", entries[0].Value) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_AllSeq_Bad(t *T) { var transaction *StoreTransaction - entries, err := ax7CollectKeyValues(transaction.AllSeq("missing")) + entries, err := fixtureCollectKeyValues(transaction.AllSeq("missing")) AssertError(t, err) AssertEmpty(t, entries) } func TestTransaction_StoreTransaction_AllSeq_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { - entries, err := ax7CollectKeyValues(transaction.AllSeq("")) + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + entries, err := fixtureCollectKeyValues(transaction.AllSeq("")) AssertNoError(t, err) AssertEmpty(t, entries) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_CountAll_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set(testTenantAConfigGroup, "a", "1")) count, err := transaction.CountAll(testTenantAPrefix) AssertNoError(t, err) AssertEqual(t, 1, count) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -839,24 +839,24 @@ func TestTransaction_StoreTransaction_CountAll_Bad(t *T) { } func TestTransaction_StoreTransaction_CountAll_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { count, err := transaction.CountAll("") AssertNoError(t, err) AssertEqual(t, 0, count) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_Groups_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "a", "1")) groups, err := transaction.Groups() AssertNoError(t, err) AssertEqual(t, []string{"config"}, groups) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -869,54 +869,54 @@ func TestTransaction_StoreTransaction_Groups_Bad(t *T) { } func TestTransaction_StoreTransaction_Groups_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { groups, err := transaction.Groups("") AssertNoError(t, err) AssertEmpty(t, groups) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_GroupsSeq_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "a", "1")) - groups, err := ax7CollectGroups(transaction.GroupsSeq()) + groups, err := fixtureCollectGroups(transaction.GroupsSeq()) AssertNoError(t, err) AssertEqual(t, []string{"config"}, groups) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_GroupsSeq_Bad(t *T) { var transaction *StoreTransaction - groups, err := ax7CollectGroups(transaction.GroupsSeq("missing")) + groups, err := fixtureCollectGroups(transaction.GroupsSeq("missing")) AssertError(t, err) AssertEmpty(t, groups) } func TestTransaction_StoreTransaction_GroupsSeq_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { - groups, err := ax7CollectGroups(transaction.GroupsSeq("")) + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { + groups, err := fixtureCollectGroups(transaction.GroupsSeq("")) AssertNoError(t, err) AssertEmpty(t, groups) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_Render_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "name", "alice")) rendered, err := transaction.Render("hello {{ .name }}", "config") AssertNoError(t, err) AssertEqual(t, "hello alice", rendered) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -929,24 +929,24 @@ func TestTransaction_StoreTransaction_Render_Bad(t *T) { } func TestTransaction_StoreTransaction_Render_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { rendered, err := transaction.Render("empty", "missing") AssertNoError(t, err) AssertEqual(t, "empty", rendered) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_GetSplit_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "hosts", "a,b")) seq, err := transaction.GetSplit("config", "hosts", ",") AssertNoError(t, err) - AssertEqual(t, []string{"a", "b"}, ax7CollectStrings(seq)) - return nil + AssertEqual(t, []string{"a", "b"}, fixtureCollectStrings(seq)) + return core.Ok(nil) }) AssertNoError(t, err) } @@ -959,25 +959,25 @@ func TestTransaction_StoreTransaction_GetSplit_Bad(t *T) { } func TestTransaction_StoreTransaction_GetSplit_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "hosts", "ab")) seq, err := transaction.GetSplit("config", "hosts", "") AssertNoError(t, err) - AssertEqual(t, []string{"a", "b"}, ax7CollectStrings(seq)) - return nil + AssertEqual(t, []string{"a", "b"}, fixtureCollectStrings(seq)) + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_GetFields_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "flags", "a b")) seq, err := transaction.GetFields("config", "flags") AssertNoError(t, err) - AssertEqual(t, []string{"a", "b"}, ax7CollectStrings(seq)) - return nil + AssertEqual(t, []string{"a", "b"}, fixtureCollectStrings(seq)) + return core.Ok(nil) }) AssertNoError(t, err) } @@ -990,25 +990,25 @@ func TestTransaction_StoreTransaction_GetFields_Bad(t *T) { } func TestTransaction_StoreTransaction_GetFields_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.Set("config", "flags", " a b ")) seq, err := transaction.GetFields("config", "flags") AssertNoError(t, err) - AssertEqual(t, []string{"a", "b"}, ax7CollectStrings(seq)) - return nil + AssertEqual(t, []string{"a", "b"}, fixtureCollectStrings(seq)) + return core.Ok(nil) }) AssertNoError(t, err) } func TestTransaction_StoreTransaction_PurgeExpired_Good(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { RequireNoError(t, transaction.SetWithTTL("session", "token", "abc", -Millisecond)) removed, err := transaction.PurgeExpired() AssertNoError(t, err) AssertEqual(t, int64(1), removed) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } @@ -1021,12 +1021,12 @@ func TestTransaction_StoreTransaction_PurgeExpired_Bad(t *T) { } func TestTransaction_StoreTransaction_PurgeExpired_Ugly(t *T) { - storeInstance := ax7Store(t) - err := storeInstance.Transaction(func(transaction *StoreTransaction) error { + storeInstance := fixtureStore(t) + err := storeInstance.Transaction(func(transaction *StoreTransaction) core.Result { removed, err := transaction.PurgeExpired() AssertNoError(t, err) AssertEqual(t, int64(0), removed) - return nil + return core.Ok(nil) }) AssertNoError(t, err) } diff --git a/go/v090_helpers_test.go b/go/v090_helpers_test.go new file mode 100644 index 0000000..69e1c42 --- /dev/null +++ b/go/v090_helpers_test.go @@ -0,0 +1,334 @@ +//nolint:unused // Compatibility helpers are used by generated v0.9 test lanes. +package store_test + +import ( + "testing" + + core "dappco.re/go" + store "dappco.re/go/store" +) + +type ( + T = core.T + Seq[V any] = core.Seq[V] + Seq2[K, V any] = core.Seq2[K, V] +) + +const ( + Hour = core.Hour + Millisecond = core.Millisecond + testMemoryDatabasePath = ":memory:" + testTenantA = "tenant-a" + testFixtureWorkspaceName = "ax7-workspace" + testFileNotFoundMessage = "file not found" +) + +var ( + AssertContains = core.AssertContains + AssertEmpty = core.AssertEmpty + AssertEqual = core.AssertEqual + AssertFalse = core.AssertFalse + AssertLen = core.AssertLen + AssertNil = core.AssertNil + AssertNotEmpty = core.AssertNotEmpty + AssertNotEqual = core.AssertNotEqual + AssertNotNil = core.AssertNotNil + AssertNotPanics = core.AssertNotPanics + AssertTrue = core.AssertTrue + HasPrefix = core.HasPrefix + NewBuffer = core.NewBuffer + NewError = core.NewError + NewReader = core.NewReader + Path = core.Path + PathBase = core.PathBase + RequireTrue = core.RequireTrue + Sprint = core.Sprint + UnixTime = core.UnixTime +) + +type ( + FileMode = core.FileMode + Fs = core.Fs + FsDirEntry = core.FsDirEntry + FsFile = core.FsFile + FsFileInfo = core.FsFileInfo + ReadCloser = core.ReadCloser + Reader = core.Reader + Time = core.Time + WriteCloser = core.WriteCloser +) + +func AssertNoError(t testing.TB, value any) { + t.Helper() + if err := resultError(value); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func RequireNoError(t testing.TB, value any) { + t.Helper() + if err := resultError(value); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func AssertError(t testing.TB, value any) { + t.Helper() + if err := resultError(value); err == nil { + t.Fatal("expected error, got nil") + } +} + +func AssertErrorIs(t testing.TB, value any, target error) { + t.Helper() + if err := resultError(value); !core.Is(err, target) { + t.Fatalf("expected error matching %v, got %v", target, value) + } +} + +func resultError(value any) error { + switch typed := value.(type) { + case nil: + return nil + case core.Result: + if typed.OK { + return nil + } + if err, ok := typed.Value.(error); ok { + return err + } + return core.E("store.test", typed.Error(), nil) + case error: + return typed + default: + return nil + } +} + +func fixtureStore(t *T) *store.Store { + t.Helper() + storeInstance, err := store.New(testMemoryDatabasePath, store.WithPurgeInterval(24*Hour)) + RequireNoError(t, err) + t.Cleanup(func() { _ = storeInstance.Close() }) + return storeInstance +} + +func fixtureConfiguredStore(t *T) (*store.Store, string) { + t.Helper() + stateDirectory := t.TempDir() + storeInstance, err := store.NewConfigured(store.StoreConfig{ + DatabasePath: testMemoryDatabasePath, + PurgeInterval: 24 * Hour, + WorkspaceStateDirectory: stateDirectory, + }) + RequireNoError(t, err) + t.Cleanup(func() { _ = storeInstance.Close() }) + return storeInstance, stateDirectory +} + +func fixtureWorkspace(t *T) (*store.Store, *store.Workspace) { + t.Helper() + storeInstance, _ := fixtureConfiguredStore(t) + workspace, err := storeInstance.NewWorkspace(testFixtureWorkspaceName) + RequireNoError(t, err) + t.Cleanup(func() { workspace.Discard() }) + return storeInstance, workspace +} + +func fixtureScopedStore(t *T) *store.ScopedStore { + t.Helper() + scopedStore, err := store.NewScopedConfigured(fixtureStore(t), store.ScopedStoreConfig{Namespace: testTenantA}) + RequireNoError(t, err) + return scopedStore +} + +func fixtureQuotaScopedStore(t *T, maxKeys, maxGroups int) *store.ScopedStore { + t.Helper() + scopedStore, err := store.NewScopedConfigured(fixtureStore(t), store.ScopedStoreConfig{ + Namespace: testTenantA, + Quota: store.QuotaConfig{MaxKeys: maxKeys, MaxGroups: maxGroups}, + }) + RequireNoError(t, err) + return scopedStore +} + +func fixtureDuckDB(t *T) *store.DuckDB { + t.Helper() + path := Path(t.TempDir(), "ax7.duckdb") + database, err := store.OpenDuckDBReadWrite(path) + RequireNoError(t, err) + t.Cleanup(func() { _ = database.Close() }) + return database +} + +func fixtureSeedDuckDB(t *T, database *store.DuckDB) { + t.Helper() + RequireNoError(t, database.Exec(`CREATE TABLE IF NOT EXISTS golden_set (idx INTEGER, seed_id VARCHAR, domain VARCHAR, voice VARCHAR, prompt VARCHAR, response VARCHAR, gen_time DOUBLE, char_count INTEGER)`)) + RequireNoError(t, database.Exec(`DELETE FROM golden_set`)) + RequireNoError(t, database.Exec(`INSERT INTO golden_set VALUES (1, 'seed-1', 'ethics', 'plain', 'prompt', 'response text', 1.5, 13)`)) + RequireNoError(t, database.Exec(`CREATE TABLE IF NOT EXISTS expansion_prompts (idx BIGINT, seed_id VARCHAR, region VARCHAR, domain VARCHAR, language VARCHAR, prompt VARCHAR, prompt_en VARCHAR, priority INTEGER, status VARCHAR)`)) + RequireNoError(t, database.Exec(`DELETE FROM expansion_prompts`)) + RequireNoError(t, database.Exec(`INSERT INTO expansion_prompts VALUES (7, 'seed-7', 'western', 'ethics', 'en', 'prompt', 'prompt en', 1, 'pending')`)) +} + +func fixtureCollectKeyValues(seq Seq2[store.KeyValue, error]) ([]store.KeyValue, error) { + var entries []store.KeyValue + for entry, err := range seq { + if err != nil { + return entries, err + } + entries = append(entries, entry) + } + return entries, nil +} + +func fixtureCollectGroups(seq Seq2[string, error]) ([]string, error) { + var groups []string + for group, err := range seq { + if err != nil { + return groups, err + } + groups = append(groups, group) + } + return groups, nil +} + +func fixtureCollectStrings(seq Seq[string]) []string { + var values []string + for value := range seq { + values = append(values, value) + } + return values +} + +func fixtureWriteFile(t *T, path, content string) { + t.Helper() + filesystem := (&Fs{}).NewUnrestricted() + result := filesystem.Write(path, content) + RequireTrue(t, result.OK) +} + +type fixtureMedium struct { + files map[string]string +} + +func newFixtureMedium() *fixtureMedium { + return &fixtureMedium{files: make(map[string]string)} +} + +func (medium *fixtureMedium) Read(path string) (string, error) { + content, ok := medium.files[path] + if !ok { + return "", NewError(testFileNotFoundMessage) + } + return content, nil +} + +func (medium *fixtureMedium) Write(path, content string) error { + medium.files[path] = content + return nil +} + +func (medium *fixtureMedium) WriteMode(path, content string, _ FileMode) error { + return medium.Write(path, content) +} + +func (medium *fixtureMedium) EnsureDir(_ string) error { return nil } +func (medium *fixtureMedium) IsFile(path string) bool { return medium.Exists(path) } +func (medium *fixtureMedium) Delete(path string) error { delete(medium.files, path); return nil } +func (medium *fixtureMedium) DeleteAll(path string) error { + for key := range medium.files { + if key == path || HasPrefix(key, path+"/") { + delete(medium.files, key) + } + } + return nil +} +func (medium *fixtureMedium) Rename(oldPath, newPath string) error { + content, ok := medium.files[oldPath] + if !ok { + return NewError(testFileNotFoundMessage) + } + medium.files[newPath] = content + delete(medium.files, oldPath) + return nil +} +func (medium *fixtureMedium) List(_ string) ([]FsDirEntry, error) { return nil, nil } +func (medium *fixtureMedium) Stat(path string) (FsFileInfo, error) { + content, ok := medium.files[path] + if !ok { + return nil, NewError(testFileNotFoundMessage) + } + return fixtureFileInfo{name: PathBase(path), size: int64(len(content))}, nil +} +func (medium *fixtureMedium) Open(path string) (FsFile, error) { + content, ok := medium.files[path] + if !ok { + return nil, NewError(testFileNotFoundMessage) + } + return &fixtureFile{name: PathBase(path), content: content, reader: NewReader(content)}, nil +} +func (medium *fixtureMedium) Create(path string) (WriteCloser, error) { + return &fixtureWriteCloser{medium: medium, path: path}, nil +} +func (medium *fixtureMedium) Append(path string) (WriteCloser, error) { + return &fixtureWriteCloser{medium: medium, path: path, content: medium.files[path]}, nil +} +func (medium *fixtureMedium) ReadStream(path string) (ReadCloser, error) { + file, err := medium.Open(path) + if err != nil { + return nil, err + } + return file.(ReadCloser), nil +} +func (medium *fixtureMedium) WriteStream(path string) (WriteCloser, error) { + return medium.Create(path) +} +func (medium *fixtureMedium) Exists(path string) bool { _, ok := medium.files[path]; return ok } +func (medium *fixtureMedium) IsDir(path string) bool { + for key := range medium.files { + if HasPrefix(key, path+"/") { + return true + } + } + return false +} + +type fixtureFile struct { + name string + content string + reader Reader +} + +func (file *fixtureFile) Read(p []byte) (int, error) { return file.reader.Read(p) } +func (file *fixtureFile) Close() error { return nil } +func (file *fixtureFile) Stat() (FsFileInfo, error) { + return fixtureFileInfo{name: file.name, size: int64(len(file.content))}, nil +} + +type fixtureWriteCloser struct { + medium *fixtureMedium + path string + content string +} + +func (writer *fixtureWriteCloser) Write(p []byte) (int, error) { + writer.content += string(p) + return len(p), nil +} +func (writer *fixtureWriteCloser) Close() error { + writer.medium.files[writer.path] = writer.content + return nil +} + +type fixtureFileInfo struct { + name string + size int64 +} + +func (info fixtureFileInfo) Name() string { return info.name } +func (info fixtureFileInfo) Size() int64 { return info.size } +func (info fixtureFileInfo) Mode() FileMode { return 0644 } +func (info fixtureFileInfo) ModTime() Time { return UnixTime(0) } +func (info fixtureFileInfo) IsDir() bool { return false } +func (info fixtureFileInfo) Sys() any { return nil } diff --git a/workspace.go b/go/workspace.go similarity index 70% rename from workspace.go rename to go/workspace.go index 3c269e1..a8786d2 100644 --- a/workspace.go +++ b/go/workspace.go @@ -71,63 +71,64 @@ func (workspace *Workspace) DatabasePath() string { // Usage example: `if err := workspace.Close(); err != nil { return }; orphans := storeInstance.RecoverOrphans(".core/state"); _ = orphans` // `Close()` keeps the `.duckdb` file on disk so `RecoverOrphans(".core/state")` // can reopen it after a crash or interrupted agent run. -func (workspace *Workspace) Close() error { +func (workspace *Workspace) Close() core.Result { return workspace.closeWithoutRemovingFiles() } -func (workspace *Workspace) ensureReady(operation string) error { +func (workspace *Workspace) ensureReady(operation string) core.Result { if workspace == nil { - return core.E(operation, "workspace is nil", nil) + return core.Fail(core.E(operation, "workspace is nil", nil)) } if workspace.store == nil { - return core.E(operation, "workspace store is nil", nil) + return core.Fail(core.E(operation, "workspace store is nil", nil)) } if workspace.db == nil { - return core.E(operation, "workspace database is nil", nil) + return core.Fail(core.E(operation, "workspace database is nil", nil)) } if workspace.filesystem == nil { - return core.E(operation, "workspace filesystem is nil", nil) + return core.Fail(core.E(operation, "workspace filesystem is nil", nil)) } - if err := workspace.store.ensureReady(operation); err != nil { - return err + if result := workspace.store.ensureReady(operation); !result.OK { + return result } workspace.lifecycleLock.Lock() closed := workspace.isClosed workspace.lifecycleLock.Unlock() if closed { - return core.E(operation, "workspace is closed", nil) + return core.Fail(core.E(operation, "workspace is closed", nil)) } - return nil + return core.Ok(nil) } // Usage example: `workspace, err := storeInstance.NewWorkspace("scroll-session-2026-03-30"); if err != nil { return }; defer workspace.Discard()` // This creates `.core/state/scroll-session-2026-03-30.duckdb` by default and // removes it when the workspace is committed or discarded. -func (storeInstance *Store) NewWorkspace(name string) (*Workspace, error) { - if err := storeInstance.ensureReady(opNewWorkspace); err != nil { - return nil, err +func (storeInstance *Store) NewWorkspace(name string) (*Workspace, core.Result) { + if result := storeInstance.ensureReady(opNewWorkspace); !result.OK { + return nil, result } workspaceNameValidation := core.ValidateName(name) if !workspaceNameValidation.OK { - return nil, core.E(opNewWorkspace, "validate workspace name", workspaceNameValidation.Value.(error)) + return nil, core.Fail(core.E(opNewWorkspace, "validate workspace name", workspaceNameValidation.Value.(error))) } filesystem := (&core.Fs{}).NewUnrestricted() stateDirectory := storeInstance.workspaceStateDirectoryPath() databasePath := workspaceFilePath(stateDirectory, name) if filesystem.Exists(databasePath) { - return nil, core.E(opNewWorkspace, core.Concat("workspace already exists: ", name), nil) + return nil, core.Fail(core.E(opNewWorkspace, core.Concat("workspace already exists: ", name), nil)) } if result := filesystem.EnsureDir(stateDirectory); !result.OK { - return nil, core.E(opNewWorkspace, "ensure state directory", result.Value.(error)) + return nil, core.Fail(core.E(opNewWorkspace, "ensure state directory", result.Value.(error))) } - database, err := openWorkspaceDatabase(databasePath) - if err != nil { - return nil, core.E(opNewWorkspace, "open workspace database", err) + database, result := openWorkspaceDatabase(databasePath) + if !result.OK { + err, _ := result.Value.(error) + return nil, core.Fail(core.E(opNewWorkspace, "open workspace database", err)) } return &Workspace{ @@ -136,7 +137,7 @@ func (storeInstance *Store) NewWorkspace(name string) (*Workspace, error) { db: database, databasePath: databasePath, filesystem: filesystem, - }, nil + }, core.Ok(nil) } // discoverOrphanWorkspacePaths(".core/state") returns leftover SQLite workspace @@ -194,8 +195,8 @@ func loadRecoveredWorkspaces(stateDirectory string, store *Store) []*Workspace { removeWorkspaceDatabaseFiles(filesystem, databasePath) continue } - database, err := openWorkspaceDatabase(databasePath) - if err != nil { + database, result := openWorkspaceDatabase(databasePath) + if !result.OK { quarantineOrphanWorkspaceFiles(filesystem, stateDirectory, databasePath) continue } @@ -206,9 +207,11 @@ func loadRecoveredWorkspaces(stateDirectory string, store *Store) []*Workspace { databasePath: databasePath, filesystem: filesystem, } - aggregate, err := orphanWorkspace.aggregateFieldsWithoutReadiness() - if err != nil { - _ = orphanWorkspace.closeWithoutRemovingFiles() + aggregate, result := orphanWorkspace.aggregateFieldsWithoutReadiness() + if !result.OK { + if closeResult := orphanWorkspace.closeWithoutRemovingFiles(); !closeResult.OK { + core.Error("orphan workspace close failed", "err", closeResult.Error()) + } quarantineOrphanWorkspaceFiles(filesystem, stateDirectory, databasePath) continue } @@ -254,39 +257,39 @@ func (storeInstance *Store) RecoverOrphans(stateDirectory string) []*Workspace { } // Usage example: `err := workspace.Put("like", map[string]any{"user": "@alice", "post": "video_123"})` -func (workspace *Workspace) Put(kind string, data map[string]any) error { - if err := workspace.ensureReady(opWorkspacePut); err != nil { - return err +func (workspace *Workspace) Put(kind string, data map[string]any) core.Result { + if result := workspace.ensureReady(opWorkspacePut); !result.OK { + return result } if kind == "" { - return core.E(opWorkspacePut, "kind is empty", nil) + return core.Fail(core.E(opWorkspacePut, "kind is empty", nil)) } if data == nil { data = map[string]any{} } - dataJSON, err := marshalJSONText(data, opWorkspacePut, "marshal entry data") - if err != nil { - return err + dataJSON, result := marshalJSONText(data, opWorkspacePut, "marshal entry data") + if !result.OK { + return result } - _, err = workspace.db.Exec( + _, execErr := workspace.db.Exec( sqlInsertIntoPrefix+workspaceEntriesTableName+workspaceEntryInsertValuesSQL, kind, dataJSON, time.Now().UnixMilli(), ) - if err != nil { - return core.E(opWorkspacePut, "insert entry", err) + if execErr != nil { + return core.Fail(core.E(opWorkspacePut, "insert entry", execErr)) } - return nil + return core.Ok(nil) } // Usage example: `entryCount, err := workspace.Count(); if err != nil { return }; fmt.Println(entryCount)` -func (workspace *Workspace) Count() (int, error) { - if err := workspace.ensureReady("store.Workspace.Count"); err != nil { - return 0, err +func (workspace *Workspace) Count() (int, core.Result) { + if result := workspace.ensureReady("store.Workspace.Count"); !result.OK { + return 0, result } var count int @@ -294,9 +297,9 @@ func (workspace *Workspace) Count() (int, error) { sqlSelectCountFrom + workspaceEntriesTableName, ).Scan(&count) if err != nil { - return 0, core.E("store.Workspace.Count", "count entries", err) + return 0, core.Fail(core.E("store.Workspace.Count", "count entries", err)) } - return count, nil + return count, core.Ok(nil) } // Usage example: `summary := workspace.Aggregate(); fmt.Println(summary["like"])` @@ -304,12 +307,12 @@ func (workspace *Workspace) Aggregate() map[string]any { if workspace.shouldUseOrphanAggregate() { return workspace.aggregateFallback() } - if err := workspace.ensureReady("store.Workspace.Aggregate"); err != nil { + if result := workspace.ensureReady("store.Workspace.Aggregate"); !result.OK { return workspace.aggregateFallback() } - fields, err := workspace.aggregateFields() - if err != nil { + fields, result := workspace.aggregateFields() + if !result.OK { return workspace.aggregateFallback() } return fields @@ -319,18 +322,19 @@ func (workspace *Workspace) Aggregate() map[string]any { // `Commit()` writes one completed workspace row to the journal, upserts the // `workspace:NAME/summary` entry, and removes the workspace file. func (workspace *Workspace) Commit() core.Result { - if err := workspace.ensureReady(opWorkspaceCommit); err != nil { - return core.Fail(err) + if result := workspace.ensureReady(opWorkspaceCommit); !result.OK { + return result } - fields, err := workspace.aggregateFields() - if err != nil { + fields, result := workspace.aggregateFields() + if !result.OK { + err, _ := result.Value.(error) return core.Fail(core.E(opWorkspaceCommit, "aggregate workspace", err)) } - if err := workspace.store.commitWorkspaceAggregate(workspace.name, fields); err != nil { - return core.Fail(err) + if result := workspace.store.commitWorkspaceAggregate(workspace.name, fields); !result.OK { + return result } - if err := workspace.closeAndRemoveFiles(); err != nil { + if result := workspace.closeAndRemoveFiles(); !result.OK { return core.Ok(cloneAnyMap(fields)) } return core.Ok(cloneAnyMap(fields)) @@ -341,15 +345,17 @@ func (workspace *Workspace) Discard() { if workspace == nil { return } - _ = workspace.closeAndRemoveFiles() + if result := workspace.closeAndRemoveFiles(); !result.OK { + core.Error("workspace discard failed", "err", result.Error()) + } } // Usage example: `result := workspace.Query("SELECT entry_kind, COUNT(*) AS count FROM workspace_entries GROUP BY entry_kind")` // `result.Value` contains `[]map[string]any`, which lets an agent inspect the // current buffer state without defining extra result types. func (workspace *Workspace) Query(query string) core.Result { - if err := workspace.ensureReady(opWorkspaceQuery); err != nil { - return core.Fail(err) + if result := workspace.ensureReady(opWorkspaceQuery); !result.OK { + return result } rows, err := workspace.db.Query(query) @@ -358,16 +364,17 @@ func (workspace *Workspace) Query(query string) core.Result { } defer func() { _ = rows.Close() }() - rowMaps, err := queryRowsAsMaps(rows) - if err != nil { + rowMaps, result := queryRowsAsMaps(rows) + if !result.OK { + err, _ := result.Value.(error) return core.Fail(core.E(opWorkspaceQuery, "scan rows", err)) } return core.Ok(rowMaps) } -func (workspace *Workspace) aggregateFields() (map[string]any, error) { - if err := workspace.ensureReady("store.Workspace.aggregateFields"); err != nil { - return nil, err +func (workspace *Workspace) aggregateFields() (map[string]any, core.Result) { + if result := workspace.ensureReady("store.Workspace.aggregateFields"); !result.OK { + return nil, result } return workspace.aggregateFieldsWithoutReadiness() } @@ -389,12 +396,12 @@ func (workspace *Workspace) shouldUseOrphanAggregate() bool { return !workspace.filesystem.Exists(workspace.databasePath) } -func (workspace *Workspace) aggregateFieldsWithoutReadiness() (map[string]any, error) { +func (workspace *Workspace) aggregateFieldsWithoutReadiness() (map[string]any, core.Result) { rows, err := workspace.db.Query( "SELECT entry_kind, COUNT(*) FROM " + workspaceEntriesTableName + " GROUP BY entry_kind ORDER BY entry_kind", ) if err != nil { - return nil, err + return nil, core.Fail(err) } defer func() { _ = rows.Close() }() @@ -405,32 +412,32 @@ func (workspace *Workspace) aggregateFieldsWithoutReadiness() (map[string]any, e count int ) if err := rows.Scan(&kind, &count); err != nil { - return nil, err + return nil, core.Fail(err) } fields[kind] = count } if err := rows.Err(); err != nil { - return nil, err + return nil, core.Fail(err) } - return fields, nil + return fields, core.Ok(nil) } -func (workspace *Workspace) closeAndRemoveFiles() error { +func (workspace *Workspace) closeAndRemoveFiles() core.Result { return workspace.closeAndCleanup(true) } // closeWithoutRemovingFiles closes the database handle but leaves the orphan // file on disk so a later store instance can recover it. -func (workspace *Workspace) closeWithoutRemovingFiles() error { +func (workspace *Workspace) closeWithoutRemovingFiles() core.Result { return workspace.closeAndCleanup(false) } -func (workspace *Workspace) closeAndCleanup(removeFiles bool) error { +func (workspace *Workspace) closeAndCleanup(removeFiles bool) core.Result { if workspace == nil { - return nil + return core.Ok(nil) } if workspace.db == nil { - return nil + return core.Ok(nil) } workspace.lifecycleLock.Lock() @@ -442,58 +449,62 @@ func (workspace *Workspace) closeAndCleanup(removeFiles bool) error { if !alreadyClosed { if err := workspace.db.Close(); err != nil { - return core.E("store.Workspace.closeAndCleanup", "close workspace database", err) + return core.Fail(core.E("store.Workspace.closeAndCleanup", "close workspace database", err)) } } if !removeFiles || workspace.filesystem == nil { - return nil + return core.Ok(nil) } for _, path := range workspaceDatabaseFilePaths(workspace.databasePath) { if result := workspace.filesystem.Delete(path); !result.OK && workspace.filesystem.Exists(path) { - return core.E("store.Workspace.closeAndCleanup", "delete workspace file", result.Value.(error)) + return core.Fail(core.E("store.Workspace.closeAndCleanup", "delete workspace file", result.Value.(error))) } } - return nil + return core.Ok(nil) } -func (storeInstance *Store) commitWorkspaceAggregate(workspaceName string, fields map[string]any) error { - if err := storeInstance.ensureReady(opWorkspaceCommit); err != nil { - return err +func (storeInstance *Store) commitWorkspaceAggregate(workspaceName string, fields map[string]any) core.Result { + if result := storeInstance.ensureReady(opWorkspaceCommit); !result.OK { + return result } - if err := ensureJournalSchema(storeInstance.sqliteDatabase); err != nil { - return core.E(opWorkspaceCommit, "ensure journal schema", err) + if result := ensureJournalSchema(storeInstance.sqliteDatabase); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opWorkspaceCommit, "ensure journal schema", err)) } transaction, err := storeInstance.sqliteDatabase.Begin() if err != nil { - return core.E(opWorkspaceCommit, "begin transaction", err) + return core.Fail(core.E(opWorkspaceCommit, "begin transaction", err)) } committed := false defer func() { if !committed { - _ = transaction.Rollback() + if rollbackErr := transaction.Rollback(); rollbackErr != nil { + core.Error("workspace commit rollback failed", "err", rollbackErr) + } } }() - fieldsJSON, err := marshalJSONText(fields, opWorkspaceCommit, "marshal summary") - if err != nil { - return err + fieldsJSON, result := marshalJSONText(fields, opWorkspaceCommit, "marshal summary") + if !result.OK { + return result } - tagsJSON, err := marshalJSONText(map[string]string{"workspace": workspaceName}, opWorkspaceCommit, "marshal tags") - if err != nil { - return err + tagsJSON, result := marshalJSONText(map[string]string{"workspace": workspaceName}, opWorkspaceCommit, "marshal tags") + if !result.OK { + return result } - if err := commitJournalEntry( + if result := commitJournalEntry( transaction, storeInstance.journalBucket(), workspaceName, fieldsJSON, tagsJSON, time.Now().UnixMilli(), - ); err != nil { - return core.E(opWorkspaceCommit, "insert journal entry", err) + ); !result.OK { + err, _ := result.Value.(error) + return core.Fail(core.E(opWorkspaceCommit, "insert journal entry", err)) } if _, err := transaction.Exec( @@ -503,11 +514,11 @@ func (storeInstance *Store) commitWorkspaceAggregate(workspaceName string, field "summary", fieldsJSON, ); err != nil { - return core.E(opWorkspaceCommit, "upsert workspace summary", err) + return core.Fail(core.E(opWorkspaceCommit, "upsert workspace summary", err)) } if err := transaction.Commit(); err != nil { - return core.E(opWorkspaceCommit, "commit transaction", err) + return core.Fail(core.E(opWorkspaceCommit, "commit transaction", err)) } committed = true storeInstance.notify(Event{ @@ -517,32 +528,40 @@ func (storeInstance *Store) commitWorkspaceAggregate(workspaceName string, field Value: fieldsJSON, Timestamp: time.Now(), }) - return nil + return core.Ok(nil) } -func openWorkspaceDatabase(databasePath string) (*sql.DB, error) { +func openWorkspaceDatabase(databasePath string) (*sql.DB, core.Result) { database, err := sql.Open("duckdb", databasePath) if err != nil { - return nil, core.E(opOpenWorkspaceDatabase, "open workspace database", err) + return nil, core.Fail(core.E(opOpenWorkspaceDatabase, "open workspace database", err)) } database.SetMaxOpenConns(1) if err := database.Ping(); err != nil { - _ = database.Close() - return nil, core.E(opOpenWorkspaceDatabase, "ping workspace database", err) + if closeErr := database.Close(); closeErr != nil { + core.Error("workspace database close after ping failed", "err", closeErr) + } + return nil, core.Fail(core.E(opOpenWorkspaceDatabase, "ping workspace database", err)) } if _, err := database.Exec("CREATE SEQUENCE IF NOT EXISTS workspace_entries_entry_id_seq START 1"); err != nil { - _ = database.Close() - return nil, core.E(opOpenWorkspaceDatabase, "create workspace entry sequence", err) + if closeErr := database.Close(); closeErr != nil { + core.Error("workspace database close after sequence failed", "err", closeErr) + } + return nil, core.Fail(core.E(opOpenWorkspaceDatabase, "create workspace entry sequence", err)) } if _, err := database.Exec(createWorkspaceEntriesTableSQL); err != nil { - _ = database.Close() - return nil, core.E(opOpenWorkspaceDatabase, "create workspace entries table", err) + if closeErr := database.Close(); closeErr != nil { + core.Error("workspace database close after table failed", "err", closeErr) + } + return nil, core.Fail(core.E(opOpenWorkspaceDatabase, "create workspace entries table", err)) } if _, err := database.Exec(createWorkspaceEntriesViewSQL); err != nil { - _ = database.Close() - return nil, core.E(opOpenWorkspaceDatabase, "create workspace entries view", err) + if closeErr := database.Close(); closeErr != nil { + core.Error("workspace database close after view failed", "err", closeErr) + } + return nil, core.Fail(core.E(opOpenWorkspaceDatabase, "create workspace entries view", err)) } - return database, nil + return database, core.Ok(nil) } func workspaceSummaryGroup(workspaceName string) string { @@ -605,8 +624,8 @@ func workspaceCommitMarkerExists(storeInstance *Store, workspaceName string) boo if storeInstance == nil || workspaceName == "" { return false } - exists, err := storeInstance.Exists(workspaceSummaryGroup(workspaceName), "summary") - return err == nil && exists + exists, result := storeInstance.Exists(workspaceSummaryGroup(workspaceName), "summary") + return result.OK && exists } func removeWorkspaceDatabaseFiles(filesystem *core.Fs, databasePath string) { @@ -614,7 +633,9 @@ func removeWorkspaceDatabaseFiles(filesystem *core.Fs, databasePath string) { return } for _, path := range workspaceDatabaseFilePaths(databasePath) { - _ = filesystem.Delete(path) + if result := filesystem.Delete(path); !result.OK { + core.Error("workspace database cleanup failed", "file", path, "err", result.Error()) + } } } @@ -629,7 +650,9 @@ func quarantineWorkspaceFile(filesystem *core.Fs, sourcePath, quarantinePath str if filesystem == nil || !filesystem.Exists(sourcePath) { return } - _ = filesystem.Rename(sourcePath, quarantinePath) + if result := filesystem.Rename(sourcePath, quarantinePath); !result.OK { + core.Error("workspace quarantine rename failed", "err", result.Error()) + } } func joinPath(base, name string) string { diff --git a/go/workspace_example_test.go b/go/workspace_example_test.go new file mode 100644 index 0000000..9e4e192 --- /dev/null +++ b/go/workspace_example_test.go @@ -0,0 +1,83 @@ +package store + +import core "dappco.re/go" + +func ExampleWorkspace_Name() { + _, workspace := exampleWorkspace() + defer workspace.Discard() + core.Println(workspace.Name()) +} + +func ExampleWorkspace_DatabasePath() { + _, workspace := exampleWorkspace() + defer workspace.Discard() + core.Println(workspace.DatabasePath()) +} + +func ExampleWorkspace_Close() { + _, workspace := exampleWorkspace() + result := workspace.Close() + exampleRequireOK(result) +} + +func ExampleStore_NewWorkspace() { + storeInstance := exampleOpenConfiguredStore() + defer exampleCloseStore(storeInstance) + workspace, result := storeInstance.NewWorkspace("example-workspace") + exampleRequireOK(result) + defer workspace.Discard() + core.Println(workspace.Name()) +} + +func ExampleStore_RecoverOrphans() { + storeInstance := exampleOpenConfiguredStore() + defer exampleCloseStore(storeInstance) + workspaces := storeInstance.RecoverOrphans(".core/example-state") + core.Println(len(workspaces)) +} + +func ExampleWorkspace_Put() { + _, workspace := exampleWorkspace() + defer workspace.Discard() + result := workspace.Put("note", map[string]any{"value": "blue"}) + exampleRequireOK(result) +} + +func ExampleWorkspace_Count() { + _, workspace := exampleWorkspace() + defer workspace.Discard() + exampleRequireOK(workspace.Put("note", map[string]any{"value": "blue"})) + count, result := workspace.Count() + exampleRequireOK(result) + core.Println(count) +} + +func ExampleWorkspace_Aggregate() { + _, workspace := exampleWorkspace() + defer workspace.Discard() + exampleRequireOK(workspace.Put("note", map[string]any{"value": "blue"})) + summary := workspace.Aggregate() + core.Println(summary) +} + +func ExampleWorkspace_Commit() { + _, workspace := exampleWorkspace() + exampleRequireOK(workspace.Put("note", map[string]any{"value": "blue"})) + result := workspace.Commit() + exampleRequireOK(result) + core.Println(result.Value) +} + +func ExampleWorkspace_Discard() { + _, workspace := exampleWorkspace() + workspace.Discard() +} + +func ExampleWorkspace_Query() { + _, workspace := exampleWorkspace() + defer workspace.Discard() + exampleRequireOK(workspace.Put("note", map[string]any{"value": "blue"})) + result := workspace.Query("SELECT entry_kind FROM workspace_entries") + exampleRequireOK(result) + core.Println(result.Value) +} diff --git a/workspace_test.go b/go/workspace_test.go similarity index 93% rename from workspace_test.go rename to go/workspace_test.go index 00faf99..201c33b 100644 --- a/workspace_test.go +++ b/go/workspace_test.go @@ -349,13 +349,13 @@ func TestWorkspace_New_Good_LeavesOrphanedWorkspacesForRecovery(t *testing.T) { orphanDatabasePath := workspaceFilePath(stateDirectory, testOrphanSession) orphanDatabase, err := openWorkspaceDatabase(orphanDatabasePath) assertNoError(t, err) - _, err = orphanDatabase.Exec( + _, sqlErr := orphanDatabase.Exec( testSQLInsertIntoPrefix+workspaceEntriesTableName+testWorkspaceEntryInsertSuffix, "like", `{"user":"@alice"}`, time.Now().UnixMilli(), ) - assertNoError(t, err) + assertNoError(t, sqlErr) assertNoError(t, orphanDatabase.Close()) assertTrue(t, testFilesystem().Exists(orphanDatabasePath)) @@ -381,13 +381,13 @@ func TestWorkspace_New_Good_CachesOrphansDuringConstruction(t *testing.T) { orphanDatabasePath := workspaceFilePath(stateDirectory, testOrphanSession) orphanDatabase, err := openWorkspaceDatabase(orphanDatabasePath) assertNoError(t, err) - _, err = orphanDatabase.Exec( + _, sqlErr := orphanDatabase.Exec( testSQLInsertIntoPrefix+workspaceEntriesTableName+testWorkspaceEntryInsertSuffix, "like", `{"user":"@alice"}`, time.Now().UnixMilli(), ) - assertNoError(t, err) + assertNoError(t, sqlErr) assertNoError(t, orphanDatabase.Close()) assertTrue(t, testFilesystem().Exists(orphanDatabasePath)) @@ -412,13 +412,13 @@ func TestWorkspace_NewConfigured_Good_CachesOrphansFromConfiguredStateDirectory( orphanDatabasePath := workspaceFilePath(stateDirectory, testOrphanSession) orphanDatabase, err := openWorkspaceDatabase(orphanDatabasePath) assertNoError(t, err) - _, err = orphanDatabase.Exec( + _, sqlErr := orphanDatabase.Exec( testSQLInsertIntoPrefix+workspaceEntriesTableName+testWorkspaceEntryInsertSuffix, "like", `{"user":"@alice"}`, time.Now().UnixMilli(), ) - assertNoError(t, err) + assertNoError(t, sqlErr) assertNoError(t, orphanDatabase.Close()) storeInstance, err := NewConfigured(StoreConfig{ @@ -490,7 +490,7 @@ func TestWorkspace_Close_Good_PreservesOrphansForRecovery(t *testing.T) { } func TestWorkspace_Store_NewWorkspace_Good(t *T) { - storeInstance, _ := ax7ConfiguredStore(t) + storeInstance, _ := fixtureConfiguredStore(t) workspace, err := storeInstance.NewWorkspace("ax7") RequireNoError(t, err) defer workspace.Discard() @@ -498,14 +498,14 @@ func TestWorkspace_Store_NewWorkspace_Good(t *T) { } func TestWorkspace_Store_NewWorkspace_Bad(t *T) { - storeInstance, _ := ax7ConfiguredStore(t) + storeInstance, _ := fixtureConfiguredStore(t) workspace, err := storeInstance.NewWorkspace("") AssertError(t, err) AssertNil(t, workspace) } func TestWorkspace_Store_NewWorkspace_Ugly(t *T) { - storeInstance, _ := ax7ConfiguredStore(t) + storeInstance, _ := fixtureConfiguredStore(t) workspace, err := storeInstance.NewWorkspace("ax7-42") RequireNoError(t, err) defer workspace.Discard() @@ -513,7 +513,7 @@ func TestWorkspace_Store_NewWorkspace_Ugly(t *T) { } func TestWorkspace_Store_RecoverOrphans_Good(t *T) { - storeInstance, stateDirectory := ax7ConfiguredStore(t) + storeInstance, stateDirectory := fixtureConfiguredStore(t) workspace, err := storeInstance.NewWorkspace("orphan") RequireNoError(t, err) RequireNoError(t, workspace.Put("entry", map[string]any{"name": "alice"})) @@ -529,15 +529,15 @@ func TestWorkspace_Store_RecoverOrphans_Bad(t *T) { } func TestWorkspace_Store_RecoverOrphans_Ugly(t *T) { - storeInstance, stateDirectory := ax7ConfiguredStore(t) + storeInstance, stateDirectory := fixtureConfiguredStore(t) orphans := storeInstance.RecoverOrphans(stateDirectory) AssertEmpty(t, orphans) } func TestWorkspace_Workspace_Name_Good(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) name := workspace.Name() - AssertEqual(t, testAX7WorkspaceName, name) + AssertEqual(t, testFixtureWorkspaceName, name) } func TestWorkspace_Workspace_Name_Bad(t *T) { @@ -547,15 +547,15 @@ func TestWorkspace_Workspace_Name_Bad(t *T) { } func TestWorkspace_Workspace_Name_Ugly(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) RequireNoError(t, workspace.Close()) - AssertEqual(t, testAX7WorkspaceName, workspace.Name()) + AssertEqual(t, testFixtureWorkspaceName, workspace.Name()) } func TestWorkspace_Workspace_DatabasePath_Good(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) path := workspace.DatabasePath() - AssertContains(t, path, testAX7WorkspaceName) + AssertContains(t, path, testFixtureWorkspaceName) } func TestWorkspace_Workspace_DatabasePath_Bad(t *T) { @@ -565,13 +565,13 @@ func TestWorkspace_Workspace_DatabasePath_Bad(t *T) { } func TestWorkspace_Workspace_DatabasePath_Ugly(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) RequireNoError(t, workspace.Close()) AssertContains(t, workspace.DatabasePath(), duckDBExtension) } func TestWorkspace_Workspace_Close_Good(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) err := workspace.Close() AssertNoError(t, err) AssertNotEmpty(t, workspace.DatabasePath()) @@ -584,34 +584,34 @@ func TestWorkspace_Workspace_Close_Bad(t *T) { } func TestWorkspace_Workspace_Close_Ugly(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) RequireNoError(t, workspace.Close()) err := workspace.Close() AssertNoError(t, err) } func TestWorkspace_Workspace_Put_Good(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) err := workspace.Put("entry", map[string]any{"name": "alice"}) AssertNoError(t, err) AssertEqual(t, 1, len(workspace.Aggregate())) } func TestWorkspace_Workspace_Put_Bad(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) err := workspace.Put("", map[string]any{"name": "alice"}) AssertError(t, err) } func TestWorkspace_Workspace_Put_Ugly(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) err := workspace.Put("entry", nil) AssertNoError(t, err) AssertEqual(t, 1, len(workspace.Aggregate())) } func TestWorkspace_Workspace_Count_Good(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) RequireNoError(t, workspace.Put("entry", map[string]any{"name": "alice"})) count, err := workspace.Count() AssertNoError(t, err) @@ -619,7 +619,7 @@ func TestWorkspace_Workspace_Count_Good(t *T) { } func TestWorkspace_Workspace_Count_Bad(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) RequireNoError(t, workspace.Close()) count, err := workspace.Count() AssertError(t, err) @@ -627,14 +627,14 @@ func TestWorkspace_Workspace_Count_Bad(t *T) { } func TestWorkspace_Workspace_Count_Ugly(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) count, err := workspace.Count() AssertNoError(t, err) AssertEqual(t, 0, count) } func TestWorkspace_Workspace_Aggregate_Good(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) RequireNoError(t, workspace.Put("entry", map[string]any{"name": "alice"})) summary := workspace.Aggregate() AssertEqual(t, "1", Sprint(summary["entry"])) @@ -647,18 +647,18 @@ func TestWorkspace_Workspace_Aggregate_Bad(t *T) { } func TestWorkspace_Workspace_Aggregate_Ugly(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) RequireNoError(t, workspace.Close()) summary := workspace.Aggregate() AssertEmpty(t, summary) } func TestWorkspace_Workspace_Commit_Good(t *T) { - storeInstance, workspace := ax7Workspace(t) + storeInstance, workspace := fixtureWorkspace(t) RequireNoError(t, workspace.Put("entry", map[string]any{"name": "alice"})) result := workspace.Commit() AssertTrue(t, result.OK) - AssertTrue(t, ax7MustGroupExists(t, storeInstance, "workspace:ax7-workspace")) + AssertTrue(t, fixtureMustGroupExists(t, storeInstance, "workspace:ax7-workspace")) } func TestWorkspace_Workspace_Commit_Bad(t *T) { @@ -669,14 +669,14 @@ func TestWorkspace_Workspace_Commit_Bad(t *T) { } func TestWorkspace_Workspace_Commit_Ugly(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) result := workspace.Commit() AssertTrue(t, result.OK) AssertEmpty(t, result.Value.(map[string]any)) } func TestWorkspace_Workspace_Discard_Good(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) workspace.Discard() result := workspace.Query("SELECT 1") AssertFalse(t, result.OK) @@ -689,13 +689,13 @@ func TestWorkspace_Workspace_Discard_Bad(t *T) { } func TestWorkspace_Workspace_Discard_Ugly(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) workspace.Discard() AssertNotPanics(t, func() { workspace.Discard() }) } func TestWorkspace_Workspace_Query_Good(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) RequireNoError(t, workspace.Put("entry", map[string]any{"name": "alice"})) result := workspace.Query("SELECT entry_kind FROM workspace_entries") AssertTrue(t, result.OK) @@ -703,14 +703,14 @@ func TestWorkspace_Workspace_Query_Good(t *T) { } func TestWorkspace_Workspace_Query_Bad(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) result := workspace.Query("SELECT * FROM missing_table") AssertFalse(t, result.OK) AssertContains(t, result.Error(), "query") } func TestWorkspace_Workspace_Query_Ugly(t *T) { - _, workspace := ax7Workspace(t) + _, workspace := fixtureWorkspace(t) result := workspace.Query("SELECT COUNT(*) AS n FROM workspace_entries") AssertTrue(t, result.OK) AssertNotEmpty(t, result.Value) diff --git a/v090_helpers_test.go b/v090_helpers_test.go deleted file mode 100644 index a2f573c..0000000 --- a/v090_helpers_test.go +++ /dev/null @@ -1,240 +0,0 @@ -//nolint:unused // Compatibility helpers are used by generated v0.9 test lanes. -package store_test - -import ( - . "dappco.re/go" - store "dappco.re/go/store" -) - -const ( - testMemoryDatabasePath = ":memory:" - testTenantA = "tenant-a" - testAX7WorkspaceName = "ax7-workspace" - testFileNotFoundMessage = "file not found" -) - -func ax7Store(t *T) *store.Store { - t.Helper() - storeInstance, err := store.New(testMemoryDatabasePath, store.WithPurgeInterval(24*Hour)) - RequireNoError(t, err) - t.Cleanup(func() { _ = storeInstance.Close() }) - return storeInstance -} - -func ax7ConfiguredStore(t *T) (*store.Store, string) { - t.Helper() - stateDirectory := t.TempDir() - storeInstance, err := store.NewConfigured(store.StoreConfig{ - DatabasePath: testMemoryDatabasePath, - PurgeInterval: 24 * Hour, - WorkspaceStateDirectory: stateDirectory, - }) - RequireNoError(t, err) - t.Cleanup(func() { _ = storeInstance.Close() }) - return storeInstance, stateDirectory -} - -func ax7Workspace(t *T) (*store.Store, *store.Workspace) { - t.Helper() - storeInstance, _ := ax7ConfiguredStore(t) - workspace, err := storeInstance.NewWorkspace(testAX7WorkspaceName) - RequireNoError(t, err) - t.Cleanup(func() { workspace.Discard() }) - return storeInstance, workspace -} - -func ax7ScopedStore(t *T) *store.ScopedStore { - t.Helper() - scopedStore, err := store.NewScopedConfigured(ax7Store(t), store.ScopedStoreConfig{Namespace: testTenantA}) - RequireNoError(t, err) - return scopedStore -} - -func ax7QuotaScopedStore(t *T, maxKeys, maxGroups int) *store.ScopedStore { - t.Helper() - scopedStore, err := store.NewScopedConfigured(ax7Store(t), store.ScopedStoreConfig{ - Namespace: testTenantA, - Quota: store.QuotaConfig{MaxKeys: maxKeys, MaxGroups: maxGroups}, - }) - RequireNoError(t, err) - return scopedStore -} - -func ax7DuckDB(t *T) *store.DuckDB { - t.Helper() - path := Path(t.TempDir(), "ax7.duckdb") - database, err := store.OpenDuckDBReadWrite(path) - RequireNoError(t, err) - t.Cleanup(func() { _ = database.Close() }) - return database -} - -func ax7SeedDuckDB(t *T, database *store.DuckDB) { - t.Helper() - RequireNoError(t, database.Exec(`CREATE TABLE IF NOT EXISTS golden_set (idx INTEGER, seed_id VARCHAR, domain VARCHAR, voice VARCHAR, prompt VARCHAR, response VARCHAR, gen_time DOUBLE, char_count INTEGER)`)) - RequireNoError(t, database.Exec(`DELETE FROM golden_set`)) - RequireNoError(t, database.Exec(`INSERT INTO golden_set VALUES (1, 'seed-1', 'ethics', 'plain', 'prompt', 'response text', 1.5, 13)`)) - RequireNoError(t, database.Exec(`CREATE TABLE IF NOT EXISTS expansion_prompts (idx BIGINT, seed_id VARCHAR, region VARCHAR, domain VARCHAR, language VARCHAR, prompt VARCHAR, prompt_en VARCHAR, priority INTEGER, status VARCHAR)`)) - RequireNoError(t, database.Exec(`DELETE FROM expansion_prompts`)) - RequireNoError(t, database.Exec(`INSERT INTO expansion_prompts VALUES (7, 'seed-7', 'western', 'ethics', 'en', 'prompt', 'prompt en', 1, 'pending')`)) -} - -func ax7CollectKeyValues(seq Seq2[store.KeyValue, error]) ([]store.KeyValue, error) { - var entries []store.KeyValue - for entry, err := range seq { - if err != nil { - return entries, err - } - entries = append(entries, entry) - } - return entries, nil -} - -func ax7CollectGroups(seq Seq2[string, error]) ([]string, error) { - var groups []string - for group, err := range seq { - if err != nil { - return groups, err - } - groups = append(groups, group) - } - return groups, nil -} - -func ax7CollectStrings(seq Seq[string]) []string { - var values []string - for value := range seq { - values = append(values, value) - } - return values -} - -func ax7WriteFile(t *T, path, content string) { - t.Helper() - filesystem := (&Fs{}).NewUnrestricted() - result := filesystem.Write(path, content) - RequireTrue(t, result.OK) -} - -type ax7Medium struct { - files map[string]string -} - -func newAX7Medium() *ax7Medium { - return &ax7Medium{files: make(map[string]string)} -} - -func (medium *ax7Medium) Read(path string) (string, error) { - content, ok := medium.files[path] - if !ok { - return "", NewError(testFileNotFoundMessage) - } - return content, nil -} - -func (medium *ax7Medium) Write(path, content string) error { - medium.files[path] = content - return nil -} - -func (medium *ax7Medium) WriteMode(path, content string, _ FileMode) error { - return medium.Write(path, content) -} - -func (medium *ax7Medium) EnsureDir(_ string) error { return nil } -func (medium *ax7Medium) IsFile(path string) bool { return medium.Exists(path) } -func (medium *ax7Medium) Delete(path string) error { delete(medium.files, path); return nil } -func (medium *ax7Medium) DeleteAll(path string) error { - for key := range medium.files { - if key == path || HasPrefix(key, path+"/") { - delete(medium.files, key) - } - } - return nil -} -func (medium *ax7Medium) Rename(oldPath, newPath string) error { - content, ok := medium.files[oldPath] - if !ok { - return NewError(testFileNotFoundMessage) - } - medium.files[newPath] = content - delete(medium.files, oldPath) - return nil -} -func (medium *ax7Medium) List(_ string) ([]FsDirEntry, error) { return nil, nil } -func (medium *ax7Medium) Stat(path string) (FsFileInfo, error) { - content, ok := medium.files[path] - if !ok { - return nil, NewError(testFileNotFoundMessage) - } - return ax7FileInfo{name: PathBase(path), size: int64(len(content))}, nil -} -func (medium *ax7Medium) Open(path string) (FsFile, error) { - content, ok := medium.files[path] - if !ok { - return nil, NewError(testFileNotFoundMessage) - } - return &ax7File{name: PathBase(path), content: content, reader: NewReader(content)}, nil -} -func (medium *ax7Medium) Create(path string) (WriteCloser, error) { - return &ax7WriteCloser{medium: medium, path: path}, nil -} -func (medium *ax7Medium) Append(path string) (WriteCloser, error) { - return &ax7WriteCloser{medium: medium, path: path, content: medium.files[path]}, nil -} -func (medium *ax7Medium) ReadStream(path string) (ReadCloser, error) { - file, err := medium.Open(path) - if err != nil { - return nil, err - } - return file.(ReadCloser), nil -} -func (medium *ax7Medium) WriteStream(path string) (WriteCloser, error) { return medium.Create(path) } -func (medium *ax7Medium) Exists(path string) bool { _, ok := medium.files[path]; return ok } -func (medium *ax7Medium) IsDir(path string) bool { - for key := range medium.files { - if HasPrefix(key, path+"/") { - return true - } - } - return false -} - -type ax7File struct { - name string - content string - reader Reader -} - -func (file *ax7File) Read(p []byte) (int, error) { return file.reader.Read(p) } -func (file *ax7File) Close() error { return nil } -func (file *ax7File) Stat() (FsFileInfo, error) { - return ax7FileInfo{name: file.name, size: int64(len(file.content))}, nil -} - -type ax7WriteCloser struct { - medium *ax7Medium - path string - content string -} - -func (writer *ax7WriteCloser) Write(p []byte) (int, error) { - writer.content += string(p) - return len(p), nil -} -func (writer *ax7WriteCloser) Close() error { - writer.medium.files[writer.path] = writer.content - return nil -} - -type ax7FileInfo struct { - name string - size int64 -} - -func (info ax7FileInfo) Name() string { return info.name } -func (info ax7FileInfo) Size() int64 { return info.size } -func (info ax7FileInfo) Mode() FileMode { return 0644 } -func (info ax7FileInfo) ModTime() Time { return UnixTime(0) } -func (info ax7FileInfo) IsDir() bool { return false } -func (info ax7FileInfo) Sys() any { return nil }