diff --git a/gorm.go b/gorm.go index eb42d648..15dec58a 100644 --- a/gorm.go +++ b/gorm.go @@ -34,7 +34,8 @@ type Config struct { DryRun bool // PrepareStmt executes the given query in cached statement PrepareStmt bool - // PrepareStmt cache support LRU expired + // PrepareStmt cache support LRU expired, + //default maxsize=int64 Max value and ttl=1h PrepareStmtMaxSize int PrepareStmtTTL time.Duration diff --git a/internal/stmt_store/stmt_store.go b/internal/stmt_store/stmt_store.go index 56c19999..7068419d 100644 --- a/internal/stmt_store/stmt_store.go +++ b/internal/stmt_store/stmt_store.go @@ -29,19 +29,71 @@ func (stmt *Stmt) Close() error { return nil } +// Store defines an interface for managing the caching operations of SQL statements (Stmt). +// This interface provides methods for creating new statements, retrieving all cache keys, +// getting cached statements, setting cached statements, and deleting cached statements. type Store interface { + // New creates a new Stmt object and caches it. + // Parameters: + // ctx: The context for the request, which can carry deadlines, cancellation signals, etc. + // key: The key representing the SQL query, used for caching and preparing the statement. + // isTransaction: Indicates whether this operation is part of a transaction, which may affect the caching strategy. + // connPool: A connection pool that provides database connections. + // locker: A synchronization lock that is unlocked after initialization to avoid deadlocks. + // Returns: + // *Stmt: A newly created statement object for executing SQL operations. + // error: An error if the statement preparation fails. New(ctx context.Context, key string, isTransaction bool, connPool ConnPool, locker sync.Locker) (*Stmt, error) + + // Keys returns a slice of all cache keys in the store. Keys() []string + + // Get retrieves a Stmt object from the store based on the given key. + // Parameters: + // key: The key used to look up the Stmt object. + // Returns: + // *Stmt: The found Stmt object, or nil if not found. + // bool: Indicates whether the corresponding Stmt object was successfully found. Get(key string) (*Stmt, bool) + + // Set stores the given Stmt object in the store and associates it with the specified key. + // Parameters: + // key: The key used to associate the Stmt object. + // value: The Stmt object to be stored. Set(key string, value *Stmt) + + // Delete removes the Stmt object corresponding to the specified key from the store. + // Parameters: + // key: The key associated with the Stmt object to be deleted. Delete(key string) } +// defaultMaxSize defines the default maximum capacity of the cache. +// Its value is the maximum value of the int64 type, which means that when the cache size is not specified, +// the cache can theoretically store as many elements as possible. +// (1 << 63) - 1 is the maximum value that an int64 type can represent. const ( defaultMaxSize = (1 << 63) - 1 - defaultTTL = time.Hour * 24 + // defaultTTL defines the default time-to-live (TTL) for each cache entry. + // When the TTL for cache entries is not specified, each cache entry will expire after 24 hours. + defaultTTL = time.Hour * 24 ) +// New creates and returns a new Store instance. +// +// Parameters: +// - size: The maximum capacity of the cache. If the provided size is less than or equal to 0, +// it defaults to defaultMaxSize. +// - ttl: The time-to-live duration for each cache entry. If the provided ttl is less than or equal to 0, +// it defaults to defaultTTL. +// +// This function defines an onEvicted callback that is invoked when a cache entry is evicted. +// The callback ensures that if the evicted value (v) is not nil, its Close method is called asynchronously +// to release associated resources. +// +// Returns: +// - A Store instance implemented by lruStore, which internally uses an LRU cache with the specified size, +// eviction callback, and TTL. func New(size int, ttl time.Duration) Store { if size <= 0 { size = defaultMaxSize @@ -87,22 +139,44 @@ type ConnPool interface { PrepareContext(ctx context.Context, query string) (*sql.Stmt, error) } +// New creates a new Stmt object for executing SQL queries. +// It caches the Stmt object for future use and handles preparation and error states. +// Parameters: +// +// ctx: Context for the request, used to carry deadlines, cancellation signals, etc. +// key: The key representing the SQL query, used for caching and preparing the statement. +// isTransaction: Indicates whether this operation is part of a transaction, affecting cache strategy. +// conn: A connection pool that provides database connections. +// locker: A synchronization lock that is unlocked after initialization to avoid deadlocks. +// +// Returns: +// +// *Stmt: A newly created statement object for executing SQL operations. +// error: An error if the statement preparation fails. func (s *lruStore) New(ctx context.Context, key string, isTransaction bool, conn ConnPool, locker sync.Locker) (_ *Stmt, err error) { + // Create a Stmt object and set its Transaction property. + // The prepared channel is used to synchronize the statement preparation state. cacheStmt := &Stmt{ Transaction: isTransaction, prepared: make(chan struct{}), } + // Cache the Stmt object with the associated key. s.Set(key, cacheStmt) + // Unlock after completing initialization to prevent deadlocks. locker.Unlock() + // Ensure the prepared channel is closed after the function execution completes. defer close(cacheStmt.prepared) + // Prepare the SQL statement using the provided connection. cacheStmt.Stmt, err = conn.PrepareContext(ctx, key) if err != nil { + // If statement preparation fails, record the error and remove the invalid Stmt object from the cache. cacheStmt.prepareErr = err s.Delete(key) return &Stmt{}, err } + // Return the successfully prepared Stmt object. return cacheStmt, nil } diff --git a/prepare_stmt.go b/prepare_stmt.go index 68c7ba69..799df5bc 100644 --- a/prepare_stmt.go +++ b/prepare_stmt.go @@ -18,12 +18,20 @@ type PreparedStmtDB struct { ConnPool } -// NewPreparedStmtDB creates a new PreparedStmtDB instance +// NewPreparedStmtDB creates and initializes a new instance of PreparedStmtDB. +// +// Parameters: +// - connPool: A connection pool that implements the ConnPool interface, used for managing database connections. +// - maxSize: The maximum number of prepared statements that can be stored in the statement store. +// - ttl: The time-to-live duration for each prepared statement in the store. Statements older than this duration will be automatically removed. +// +// Returns: +// - A pointer to a PreparedStmtDB instance, which manages prepared statements using the provided connection pool and configuration. func NewPreparedStmtDB(connPool ConnPool, maxSize int, ttl time.Duration) *PreparedStmtDB { return &PreparedStmtDB{ - ConnPool: connPool, - Stmts: stmt_store.New(maxSize, ttl), - Mux: &sync.RWMutex{}, + ConnPool: connPool, // Assigns the provided connection pool to manage database connections. + Stmts: stmt_store.New(maxSize, ttl), // Initializes a new statement store with the specified maximum size and TTL. + Mux: &sync.RWMutex{}, // Sets up a read-write mutex for synchronizing access to the statement store. } } diff --git a/tests/lru_test.go b/tests/lru_test.go new file mode 100644 index 00000000..5f431648 --- /dev/null +++ b/tests/lru_test.go @@ -0,0 +1,60 @@ +package tests_test + +import ( + "gorm.io/gorm/internal/lru" + "testing" + "time" +) + +func TestLRU_Add_ExistingKey_UpdatesValueAndExpiresAt(t *testing.T) { + lru := lru.NewLRU[string, int](10, nil, time.Hour) + lru.Add("key1", 1) + lru.Add("key1", 2) + + if value, ok := lru.Get("key1"); !ok || value != 2 { + t.Errorf("Expected value to be updated to 2, got %v", value) + } +} + +func TestLRU_Add_NewKey_AddsEntry(t *testing.T) { + lru := lru.NewLRU[string, int](10, nil, time.Hour) + lru.Add("key1", 1) + + if value, ok := lru.Get("key1"); !ok || value != 1 { + t.Errorf("Expected key1 to be added with value 1, got %v", value) + } +} + +func TestLRU_Add_ExceedsSize_RemovesOldest(t *testing.T) { + lru := lru.NewLRU[string, int](2, nil, time.Hour) + lru.Add("key1", 1) + lru.Add("key2", 2) + lru.Add("key3", 3) + + if _, ok := lru.Get("key1"); ok { + t.Errorf("Expected key1 to be removed, but it still exists") + } +} + +func TestLRU_Add_UnlimitedSize_NoEviction(t *testing.T) { + lru := lru.NewLRU[string, int](0, nil, time.Hour) + lru.Add("key1", 1) + lru.Add("key2", 2) + lru.Add("key3", 3) + + if _, ok := lru.Get("key1"); !ok { + t.Errorf("Expected key1 to exist, but it was evicted") + } +} + +func TestLRU_Add_Eviction(t *testing.T) { + lru := lru.NewLRU[string, int](0, nil, time.Second*2) + lru.Add("key1", 1) + lru.Add("key2", 2) + lru.Add("key3", 3) + time.Sleep(time.Second * 3) + if lru.Cap() != 0 { + t.Errorf("Expected lru to be empty, but it was not") + } + +}