Files
fn_registry/functions/infra/cache_to_sqlite.go
T
egutierrez 9c0d24d3ef feat: funciones Go — core (cron, join_by_key, validate_struct), datascience (pivot, diff_entities), infra (http, cache, cron_ticker)
Nuevas funciones Go con tests en tres dominios:
- core: parse_cron_expr, next_cron_time, join_by_key, validate_struct_fields + tipo CronSchedule
- datascience: pivot (tabla dinámica), diff_entities (comparación de entidades)
- infra: http_get_json, http_post_json, http_download_file, cache_to_sqlite, cron_ticker

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:11:12 +02:00

157 lines
4.2 KiB
Go

package infra
import (
"database/sql"
"encoding/json"
"fmt"
"sync"
"time"
_ "github.com/mattn/go-sqlite3"
)
// SQLiteCache es un cache key-value persistido en SQLite con soporte de TTL.
// Valores almacenados como JSON serializado. El caller es responsable de
// deserializar el []byte retornado por Get.
// Seguro para uso concurrente.
type SQLiteCache struct {
db *sql.DB
namespace string
mu sync.Mutex
}
const sqliteCacheSchema = `
CREATE TABLE IF NOT EXISTS cache (
namespace TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
created_at REAL NOT NULL,
expires_at REAL,
PRIMARY KEY (namespace, key)
);`
// CacheToSQLite abre (o crea) una base de datos SQLite en dbPath y retorna
// un SQLiteCache para el namespace dado.
func CacheToSQLite(dbPath, namespace string) (*SQLiteCache, error) {
db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL")
if err != nil {
return nil, fmt.Errorf("cache_to_sqlite: open db: %w", err)
}
if _, err := db.Exec(sqliteCacheSchema); err != nil {
db.Close()
return nil, fmt.Errorf("cache_to_sqlite: create schema: %w", err)
}
return &SQLiteCache{db: db, namespace: namespace}, nil
}
// evictExpired elimina las entradas expiradas del namespace. Debe llamarse
// con el mutex ya tomado.
func (c *SQLiteCache) evictExpired() {
now := float64(time.Now().UnixNano()) / 1e9
c.db.Exec(
"DELETE FROM cache WHERE namespace = ? AND expires_at IS NOT NULL AND expires_at <= ?",
c.namespace, now,
)
}
// Get retorna el valor asociado a key y true, o nil y false si no existe o
// esta expirado. El []byte contiene JSON que el caller puede deserializar.
func (c *SQLiteCache) Get(key string) ([]byte, bool) {
c.mu.Lock()
defer c.mu.Unlock()
c.evictExpired()
var value string
err := c.db.QueryRow(
"SELECT value FROM cache WHERE namespace = ? AND key = ?",
c.namespace, key,
).Scan(&value)
if err != nil {
return nil, false
}
return []byte(value), true
}
// Set almacena value (JSON bytes) bajo key. ttl=0 significa sin expiracion.
func (c *SQLiteCache) Set(key string, value []byte, ttl time.Duration) error {
c.mu.Lock()
defer c.mu.Unlock()
now := float64(time.Now().UnixNano()) / 1e9
var expiresAt any
if ttl > 0 {
expiresAt = now + ttl.Seconds()
}
_, err := c.db.Exec(
`INSERT INTO cache (namespace, key, value, created_at, expires_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(namespace, key) DO UPDATE SET
value = excluded.value,
created_at = excluded.created_at,
expires_at = excluded.expires_at`,
c.namespace, key, string(value), now, expiresAt,
)
if err != nil {
return fmt.Errorf("cache set: %w", err)
}
return nil
}
// Delete elimina la entrada asociada a key. Retorna error si falla la query.
func (c *SQLiteCache) Delete(key string) error {
c.mu.Lock()
defer c.mu.Unlock()
_, err := c.db.Exec(
"DELETE FROM cache WHERE namespace = ? AND key = ?",
c.namespace, key,
)
if err != nil {
return fmt.Errorf("cache delete: %w", err)
}
return nil
}
// Clear elimina todas las entradas del namespace. Retorna el numero de filas
// eliminadas.
func (c *SQLiteCache) Clear() (int64, error) {
c.mu.Lock()
defer c.mu.Unlock()
res, err := c.db.Exec(
"DELETE FROM cache WHERE namespace = ?",
c.namespace,
)
if err != nil {
return 0, fmt.Errorf("cache clear: %w", err)
}
n, _ := res.RowsAffected()
return n, nil
}
// GetOrSet retorna el valor cacheado o llama factory() para obtenerlo,
// lo almacena con el ttl dado y lo retorna.
func (c *SQLiteCache) GetOrSet(key string, factory func() ([]byte, error), ttl time.Duration) ([]byte, error) {
if v, ok := c.Get(key); ok {
return v, nil
}
value, err := factory()
if err != nil {
return nil, fmt.Errorf("cache get_or_set factory: %w", err)
}
if err := c.Set(key, value, ttl); err != nil {
return nil, err
}
return value, nil
}
// SetJSON serializa v como JSON y lo almacena bajo key.
func (c *SQLiteCache) SetJSON(key string, v any, ttl time.Duration) error {
b, err := json.Marshal(v)
if err != nil {
return fmt.Errorf("cache set_json marshal: %w", err)
}
return c.Set(key, b, ttl)
}
// Close cierra la conexion a la base de datos.
func (c *SQLiteCache) Close() error {
return c.db.Close()
}