Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a37196f
Implemented optional thread-safe caching to database
Valentin-Vi Dec 15, 2025
3dc3601
Implemented optional thread-safe caching for database
Valentin-Vi Dec 15, 2025
5749d44
Formatted
Valentin-Vi Dec 15, 2025
0b552c6
Wrote more tests to satisfy -gtl 90 percent test coverage
Valentin-Vi Dec 15, 2025
2fcf809
Checks in TestGoCache_Get_UnmarshalError test were flawd. It intended…
Valentin-Vi Dec 16, 2025
46bc3ff
Ran and Formatting Go code...
Valentin-Vi Dec 16, 2025
8a43f6b
Removed default config from default.yaml and defined it as constant v…
Valentin-Vi Dec 17, 2025
4f4406d
Removed unnecessary in-memory cache implementation
Valentin-Vi Dec 18, 2025
ce9db6f
Removed unnecessary tests for removed in-memory cache feature
Valentin-Vi Dec 18, 2025
ffbcdc7
Added integration tests
Valentin-Vi Dec 19, 2025
af918ed
Minor changes ;)
Valentin-Vi Dec 21, 2025
e6de5d6
Minor changes ;)
Valentin-Vi Dec 21, 2025
6640ae9
Streamlined cached_repository.go
Valentin-Vi Dec 21, 2025
2b1f675
The cached repository now uses dependency injection for its base repo…
Valentin-Vi Dec 21, 2025
9066ae8
Removed comments from tests
Valentin-Vi Dec 23, 2025
cf9df25
Added Where method in internal/core/database/cache/cached_repository.…
Valentin-Vi Dec 30, 2025
9fe2e14
Took test coverage of internal/core/database/cache/cached_repository.…
Valentin-Vi Jan 6, 2026
4217642
Removed Watcher logging at internal/core/database/cache/cached_reposi…
Valentin-Vi Jan 6, 2026
a898f2e
Merge branch 'develop' into feature/cache-abstraction-layer
Valentin-Vi Jan 6, 2026
bf248c0
Eliminated use of reflection, replacing it with a param which asks …
Valentin-Vi Jan 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/gin-gonic/gin v1.11.0
github.com/go-playground/validator/v10 v10.30.1
github.com/google/uuid v1.6.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/shirou/gopsutil/v3 v3.24.5
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.11.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
16 changes: 16 additions & 0 deletions internal/core/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,19 @@ type Watcher struct {
Compress bool `yaml:"compress"`
}

type Cache struct {
Enabled bool `yaml:"enabled"`
DefaultTTL string `yaml:"default_ttl"`
CleanupInterval string `yaml:"cleanup_interval"`
}

type ConfigData struct {
Netbridge Netbridge `yaml:"netbridge"`
Arrows Arrows `yaml:"arrows"`
API API `yaml:"api"`
Database Database `yaml:"database"`
Watcher Watcher `yaml:"watcher"`
Cache Cache `yaml:"cache"`
}

type Config struct {
Expand Down Expand Up @@ -99,6 +106,10 @@ func GetWatcher() Watcher {
return Get().Config.Watcher
}

func GetCache() Cache {
return Get().Config.Cache
}

func GetConfigPath() string {
return metadata.GetDefaultConfigPath()
}
Expand Down Expand Up @@ -143,6 +154,11 @@ func getDefaultConfig() *Config {
MaxAge: 7,
Compress: true,
},
Cache: Cache{
Enabled: false,
DefaultTTL: "5m",
CleanupInterval: "1m",
},
},
}
}
Expand Down
49 changes: 49 additions & 0 deletions internal/core/database/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package database

import (
"context"

"github.com/rabbytesoftware/quiver/internal/core/database/cache"
interfaces "github.com/rabbytesoftware/quiver/internal/core/database/interface"
"github.com/rabbytesoftware/quiver/internal/core/database/repository"

dberr "github.com/rabbytesoftware/quiver/internal/core/database/error"
)

type DatabaseBuilder[T any] struct {
name string
cacheConfig *cache.CacheConfig
extractKey func(entity *T) (string, error)
}

func NewDatabaseBuilder[T any](
ctx context.Context,
name string,
) *DatabaseBuilder[T] {
return &DatabaseBuilder[T]{
name: name,
}
}

func (b *DatabaseBuilder[T]) WithCache(config cache.CacheConfig, extractKey func(entity *T) (string, error)) *DatabaseBuilder[T] {
b.cacheConfig = &config
b.extractKey = extractKey
return b
}

func (b *DatabaseBuilder[T]) Build() (interfaces.RepositoryInterface[T], error) {
if b.name == "" {
return nil, dberr.ErrNameRequired
}

repo, err := repository.NewRepository[T](b.name)
if err != nil {
return nil, err
}

if b.cacheConfig != nil && b.cacheConfig.IsValid() {
return cache.NewCachedRepository[T](repo, *b.cacheConfig, b.extractKey)
}

return repo, nil
}
202 changes: 202 additions & 0 deletions internal/core/database/builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package database

import (
"context"
"testing"
"time"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/rabbytesoftware/quiver/internal/core/database/cache"
interfaces "github.com/rabbytesoftware/quiver/internal/core/database/interface"
)

type BuilderTestEntity struct {
ID uuid.UUID `gorm:"type:uuid;primary_key" json:"id"`
Name string `gorm:"not null" json:"name"`
}

func (BuilderTestEntity) TableName() string {
return "builder_test_entities"
}

// builderTestEntityExtractKey extracts the ID as a string for cache keys
func builderTestEntityExtractKey(entity *BuilderTestEntity) (string, error) {
if entity == nil {
return "", nil
}
return entity.ID.String(), nil
}

func TestDatabaseBuilder_Build_WithoutCache(t *testing.T) {
ctx := context.Background()
tempDir := t.TempDir()
t.Setenv("QUIVER_DATABASE_PATH", tempDir)

db, err := NewDatabaseBuilder[BuilderTestEntity](ctx, "test_no_cache").
Build()

require.NoError(t, err)
assert.NotNil(t, db)

t.Cleanup(func() {
_ = db.Close()
})
}

func TestDatabaseBuilder_Build_WithCache(t *testing.T) {
ctx := context.Background()
tempDir := t.TempDir()
t.Setenv("QUIVER_DATABASE_PATH", tempDir)

cacheConfig := cache.CacheConfig{
DefaultTTL: 5 * time.Minute,
CleanupInterval: 1 * time.Minute,
}

db, err := NewDatabaseBuilder[BuilderTestEntity](ctx, "test_with_cache").
WithCache(cacheConfig, builderTestEntityExtractKey).
Build()

require.NoError(t, err)
assert.NotNil(t, db)

t.Cleanup(func() {
_ = db.Close()
})
}

func TestDatabaseBuilder_Build_WithDefaultCacheConfig(t *testing.T) {
ctx := context.Background()
tempDir := t.TempDir()
t.Setenv("QUIVER_DATABASE_PATH", tempDir)

cacheConfig := cache.DefaultCacheConfig()

db, err := NewDatabaseBuilder[BuilderTestEntity](ctx, "test_default_cache").
WithCache(cacheConfig, builderTestEntityExtractKey).
Build()

require.NoError(t, err)
assert.NotNil(t, db)

entity := &BuilderTestEntity{ID: uuid.New(), Name: "Test"}
created, err := db.Create(ctx, entity)
require.NoError(t, err)

retrieved, err := db.GetByID(ctx, created.ID)
require.NoError(t, err)
assert.Equal(t, created.Name, retrieved.Name)

t.Cleanup(func() {
_ = db.Close()
})
}

func TestDatabaseBuilder_Build_InvalidCacheConfig_FallsBackToNonCached(t *testing.T) {
ctx := context.Background()
tempDir := t.TempDir()
t.Setenv("QUIVER_DATABASE_PATH", tempDir)

cacheConfig := cache.CacheConfig{
DefaultTTL: 0,
CleanupInterval: time.Minute,
}

db, err := NewDatabaseBuilder[BuilderTestEntity](ctx, "test_invalid_cache").
WithCache(cacheConfig, builderTestEntityExtractKey).
Build()

require.NoError(t, err)
assert.NotNil(t, db)

entity := &BuilderTestEntity{ID: uuid.New(), Name: "Test"}
created, err := db.Create(ctx, entity)
require.NoError(t, err)

retrieved, err := db.GetByID(ctx, created.ID)
require.NoError(t, err)
assert.Equal(t, created.Name, retrieved.Name)

t.Cleanup(func() {
_ = db.Close()
})
}

func TestDatabaseBuilder_Chaining(t *testing.T) {
ctx := context.Background()
tempDir := t.TempDir()
t.Setenv("QUIVER_DATABASE_PATH", tempDir)

cacheConfig := cache.CacheConfig{
DefaultTTL: 5 * time.Minute,
CleanupInterval: 1 * time.Minute,
}

builder := NewDatabaseBuilder[BuilderTestEntity](ctx, "test_chaining")
builder = builder.WithCache(cacheConfig, builderTestEntityExtractKey)
db, err := builder.Build()

require.NoError(t, err)
assert.NotNil(t, db)

t.Cleanup(func() {
_ = db.Close()
})
}

func TestDatabaseBuilder_Build_ReturnsRepositoryInterface(t *testing.T) {
ctx := context.Background()
tempDir := t.TempDir()
t.Setenv("QUIVER_DATABASE_PATH", tempDir)

db, err := NewDatabaseBuilder[BuilderTestEntity](ctx, "test_interface").Build()

require.NoError(t, err)

var _ interfaces.RepositoryInterface[BuilderTestEntity] = db

t.Cleanup(func() {
_ = db.Close()
})
}

func TestNewDatabase_StillWorks(t *testing.T) {
ctx := context.Background()
tempDir := t.TempDir()
t.Setenv("QUIVER_DATABASE_PATH", tempDir)

db, err := NewDatabase[BuilderTestEntity](ctx, "test_backwards_compat")

require.NoError(t, err)
assert.NotNil(t, db)

t.Cleanup(func() {
_ = db.Close()
})
}

func TestDatabaseBuilder_MultipleBuilds(t *testing.T) {
ctx := context.Background()
tempDir := t.TempDir()
t.Setenv("QUIVER_DATABASE_PATH", tempDir)

builder := NewDatabaseBuilder[BuilderTestEntity](ctx, "test_multiple")

db1, err1 := builder.Build()
db2, err2 := builder.Build()

require.NoError(t, err1)
require.NoError(t, err2)
assert.NotNil(t, db1)
assert.NotNil(t, db2)

assert.NotEqual(t, db1, db2)

t.Cleanup(func() {
_ = db1.Close()
_ = db2.Close()
})
}
Loading
Loading