feat: abstracción DB multi-engine — CRUD genérico y openers para SQLite, Postgres, ClickHouse, DuckDB

Funciones Go con interfaz unificada para operaciones DB: open, close, create_table, exec, query, insert_row, insert_batch.
Openers específicos por engine. Tipo DBConfig para configuración común.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 20:55:17 +02:00
parent c33e907fef
commit a75170cbc6
22 changed files with 787 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
package infra
import (
"database/sql"
"fmt"
_ "github.com/ClickHouse/clickhouse-go/v2"
)
// ClickHouseOpen connects to a ClickHouse server and returns a *sql.DB.
// Constructs a DSN of the form:
//
// clickhouse://user:password@host:port/database
func ClickHouseOpen(host string, port int, user, password, database string) (*sql.DB, error) {
dsn := fmt.Sprintf("clickhouse://%s:%s@%s:%d/%s", user, password, host, port, database)
db, err := sql.Open("clickhouse", dsn)
if err != nil {
return nil, fmt.Errorf("clickhouse_open: open: %w", err)
}
if err := db.Ping(); err != nil {
db.Close()
return nil, fmt.Errorf("clickhouse_open: ping %s:%d/%s: %w", host, port, database, err)
}
return db, nil
}
+37
View File
@@ -0,0 +1,37 @@
---
name: clickhouse_open
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func ClickHouseOpen(host string, port int, user, password, database string) (*sql.DB, error)"
description: "Conecta a ClickHouse construyendo DSN clickhouse://user:pass@host:port/database."
tags: [database, clickhouse, connection, sql, olap]
uses_functions: []
uses_types: [db_config_go_infra]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["database/sql", "github.com/ClickHouse/clickhouse-go/v2"]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/clickhouse_open.go"
---
## Ejemplo
```go
db, err := ClickHouseOpen("localhost", 9000, "default", "", "analytics")
if err != nil {
log.Fatal(err)
}
defer DBClose(db)
rows, err := DBQuery(db, "SELECT event, count() FROM events GROUP BY event")
```
## Notas
Usa el driver `github.com/ClickHouse/clickhouse-go/v2` registrado como "clickhouse". Puerto por defecto de ClickHouse es 9000 (nativo) o 8123 (HTTP). Hace ping al abrir para verificar conectividad.
+18
View File
@@ -0,0 +1,18 @@
package infra
import (
"database/sql"
"fmt"
)
// DBClose closes the database connection. Wraps db.Close() for composability
// in pipelines that manage *sql.DB lifecycle explicitly.
func DBClose(db *sql.DB) error {
if db == nil {
return fmt.Errorf("db_close: db is nil")
}
if err := db.Close(); err != nil {
return fmt.Errorf("db_close: %w", err)
}
return nil
}
+35
View File
@@ -0,0 +1,35 @@
---
name: db_close
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func DBClose(db *sql.DB) error"
description: "Cierra la conexion a la base de datos. Wrapper sobre db.Close() para composabilidad en pipelines que gestionan el ciclo de vida de *sql.DB explicitamente."
tags: [database, sql, close, lifecycle]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["database/sql"]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/db_close.go"
---
## Ejemplo
```go
db, err := SQLiteOpen("/data/app.db")
if err != nil {
return err
}
defer DBClose(db)
```
## Notas
Retorna error si db es nil. En la mayoria de los casos se usa con `defer`. Existe como funcion del registry para que los pipelines puedan referenciarla en `uses_functions` y modelar el ciclo de vida completo de la conexion.
+8
View File
@@ -0,0 +1,8 @@
package infra
// DBConfig holds connection parameters for any supported database.
type DBConfig struct {
Driver string // "sqlite", "duckdb", "postgres", "clickhouse"
DSN string // Data source name / connection string
Opts map[string]string // Driver-specific options (optional)
}
+32
View File
@@ -0,0 +1,32 @@
package infra
import (
"database/sql"
"fmt"
"regexp"
"strings"
)
var validIdentifier = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
// DBCreateTable executes CREATE TABLE IF NOT EXISTS for the given table with
// the provided column definitions. Each element of columns should be a full
// SQL column definition, e.g. "id INTEGER PRIMARY KEY" or "name TEXT NOT NULL".
// Returns an error if the table name contains invalid characters.
func DBCreateTable(db *sql.DB, table string, columns []string) error {
if !validIdentifier.MatchString(table) {
return fmt.Errorf("db_create_table: invalid table name %q (only alphanumeric and underscore allowed)", table)
}
if len(columns) == 0 {
return fmt.Errorf("db_create_table: at least one column definition required")
}
query := fmt.Sprintf(
"CREATE TABLE IF NOT EXISTS %s (%s)",
table,
strings.Join(columns, ", "),
)
if _, err := db.Exec(query); err != nil {
return fmt.Errorf("db_create_table %q: %w", table, err)
}
return nil
}
+36
View File
@@ -0,0 +1,36 @@
---
name: db_create_table
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func DBCreateTable(db *sql.DB, table string, columns []string) error"
description: "Ejecuta CREATE TABLE IF NOT EXISTS con las definiciones de columnas dadas. Valida que el nombre de tabla sea un identificador SQL seguro."
tags: [database, sql, ddl, create, table, schema]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["database/sql", "regexp", "strings"]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/db_create_table.go"
---
## Ejemplo
```go
err := DBCreateTable(db, "events", []string{
"id INTEGER PRIMARY KEY AUTOINCREMENT",
"name TEXT NOT NULL",
"ts INTEGER NOT NULL",
"payload TEXT",
})
```
## Notas
`columns` son definiciones SQL completas incluyendo nombre, tipo y constraints. Usa `CREATE TABLE IF NOT EXISTS` para ser idempotente. Valida el nombre de tabla con regex `^[a-zA-Z_][a-zA-Z0-9_]*$` para prevenir SQL injection. Las definiciones de columna no se sanitizan — son responsabilidad del llamador.
+22
View File
@@ -0,0 +1,22 @@
package infra
import (
"database/sql"
"fmt"
)
// DBExec executes a non-SELECT statement (INSERT, UPDATE, DELETE, DDL) and
// returns the number of rows affected. For statements that don't return rows
// affected (e.g. DDL on some drivers), the count may be 0.
func DBExec(db *sql.DB, query string, args ...any) (int64, error) {
result, err := db.Exec(query, args...)
if err != nil {
return 0, fmt.Errorf("db_exec: %w", err)
}
n, err := result.RowsAffected()
if err != nil {
// Some drivers don't support RowsAffected; treat as 0.
return 0, nil
}
return n, nil
}
+35
View File
@@ -0,0 +1,35 @@
---
name: db_exec
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func DBExec(db *sql.DB, query string, args ...any) (int64, error)"
description: "Ejecuta un statement no-SELECT (INSERT, UPDATE, DELETE, DDL) y retorna el numero de filas afectadas."
tags: [database, sql, exec, insert, update, delete, ddl]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["database/sql"]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/db_exec.go"
---
## Ejemplo
```go
n, err := DBExec(db, "UPDATE users SET active = ? WHERE last_login < ?", false, cutoff)
if err != nil {
return err
}
fmt.Printf("desactivados: %d usuarios\n", n)
```
## Notas
Agnóstica al driver. Para DDL algunos drivers retornan 0 en RowsAffected — esto es normal. Para INSERT con last_insert_id usar `DBInsertRow` que retorna ese valor. Para multiples filas en una transaccion usar `DBInsertBatch`.
+70
View File
@@ -0,0 +1,70 @@
package infra
import (
"database/sql"
"fmt"
"strings"
)
// DBInsertBatch inserts multiple rows into a table using a prepared statement
// inside a transaction. columns must match the order of values in each row.
// Returns the total number of rows affected.
// Column and table names are validated to contain only safe identifier chars.
func DBInsertBatch(db *sql.DB, table string, columns []string, rows [][]any) (int64, error) {
if !validIdentifier.MatchString(table) {
return 0, fmt.Errorf("db_insert_batch: invalid table name %q", table)
}
if len(columns) == 0 {
return 0, fmt.Errorf("db_insert_batch: columns must not be empty")
}
if len(rows) == 0 {
return 0, nil
}
for _, col := range columns {
if !validIdentifier.MatchString(col) {
return 0, fmt.Errorf("db_insert_batch: invalid column name %q", col)
}
}
placeholders := make([]string, len(columns))
for i := range columns {
placeholders[i] = "?"
}
query := fmt.Sprintf(
"INSERT INTO %s (%s) VALUES (%s)",
table,
strings.Join(columns, ", "),
strings.Join(placeholders, ", "),
)
tx, err := db.Begin()
if err != nil {
return 0, fmt.Errorf("db_insert_batch: begin tx: %w", err)
}
defer tx.Rollback() //nolint:errcheck
stmt, err := tx.Prepare(query)
if err != nil {
return 0, fmt.Errorf("db_insert_batch: prepare: %w", err)
}
defer stmt.Close()
var total int64
for i, row := range rows {
if len(row) != len(columns) {
return 0, fmt.Errorf("db_insert_batch: row %d has %d values, expected %d", i, len(row), len(columns))
}
result, err := stmt.Exec(row...)
if err != nil {
return 0, fmt.Errorf("db_insert_batch: exec row %d: %w", i, err)
}
n, _ := result.RowsAffected()
total += n
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("db_insert_batch: commit: %w", err)
}
return total, nil
}
+41
View File
@@ -0,0 +1,41 @@
---
name: db_insert_batch
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func DBInsertBatch(db *sql.DB, table string, columns []string, rows [][]any) (int64, error)"
description: "Inserta multiples filas en una transaccion usando prepared statement. Retorna el total de filas afectadas. Mas eficiente que llamar DBInsertRow en un loop."
tags: [database, sql, insert, batch, transaction, bulk]
uses_functions: [db_insert_row_go_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["database/sql", "strings"]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/db_insert_batch.go"
---
## Ejemplo
```go
cols := []string{"name", "score", "ts"}
rows := [][]any{
{"Alice", 95.5, 1700000000},
{"Bob", 87.2, 1700000001},
{"Carol", 91.0, 1700000002},
}
n, err := DBInsertBatch(db, "results", cols, rows)
if err != nil {
return err
}
fmt.Printf("insertadas: %d filas\n", n)
```
## Notas
Usa `tx.Prepare()` + `stmt.Exec()` en un loop dentro de una transaccion. El rollback es automatico si alguna fila falla. Valida tabla y columnas con regex. Cada fila debe tener exactamente `len(columns)` valores — retorna error descriptivo si no coincide.
+54
View File
@@ -0,0 +1,54 @@
package infra
import (
"database/sql"
"fmt"
"sort"
"strings"
)
// DBInsertRow generates and executes a single-row INSERT from a map of
// column→value pairs. Returns the last insert ID reported by the driver.
// Column and table names are validated to contain only safe identifier chars.
func DBInsertRow(db *sql.DB, table string, row map[string]any) (int64, error) {
if !validIdentifier.MatchString(table) {
return 0, fmt.Errorf("db_insert_row: invalid table name %q", table)
}
if len(row) == 0 {
return 0, fmt.Errorf("db_insert_row: row map must not be empty")
}
// Sort keys for deterministic query generation.
cols := make([]string, 0, len(row))
for col := range row {
if !validIdentifier.MatchString(col) {
return 0, fmt.Errorf("db_insert_row: invalid column name %q", col)
}
cols = append(cols, col)
}
sort.Strings(cols)
placeholders := make([]string, len(cols))
values := make([]any, len(cols))
for i, col := range cols {
placeholders[i] = "?"
values[i] = row[col]
}
query := fmt.Sprintf(
"INSERT INTO %s (%s) VALUES (%s)",
table,
strings.Join(cols, ", "),
strings.Join(placeholders, ", "),
)
result, err := db.Exec(query, values...)
if err != nil {
return 0, fmt.Errorf("db_insert_row %q: %w", table, err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, nil
}
return id, nil
}
+39
View File
@@ -0,0 +1,39 @@
---
name: db_insert_row
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func DBInsertRow(db *sql.DB, table string, row map[string]any) (int64, error)"
description: "Genera y ejecuta un INSERT de una sola fila desde un map columna→valor. Retorna el last insert ID. Sanitiza nombres de tabla y columnas."
tags: [database, sql, insert, row, dynamic]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["database/sql", "sort", "strings"]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/db_insert_row.go"
---
## Ejemplo
```go
id, err := DBInsertRow(db, "users", map[string]any{
"name": "Alice",
"email": "alice@example.com",
"active": true,
})
if err != nil {
return err
}
fmt.Printf("nuevo usuario ID: %d\n", id)
```
## Notas
Las claves del map se ordenan alfabeticamente para generar queries deterministas. Valida tabla y columnas con regex `^[a-zA-Z_][a-zA-Z0-9_]*$`. Para insertar muchas filas usar `DBInsertBatch` que es mas eficiente. El last insert ID puede ser 0 en drivers que no lo soportan (ej: postgres — usar RETURNING en su lugar con `DBQuery`).
+79
View File
@@ -0,0 +1,79 @@
package infra
import (
"database/sql"
"fmt"
"strconv"
)
// DBQuery executes a SELECT query and returns the results as a slice of maps.
// Each map key is the column name; values are converted to native Go types:
// int64, float64, bool, string, []byte, or nil for NULLs.
func DBQuery(db *sql.DB, query string, args ...any) ([]map[string]any, error) {
rows, err := db.Query(query, args...)
if err != nil {
return nil, fmt.Errorf("db_query: %w", err)
}
defer rows.Close()
cols, err := rows.Columns()
if err != nil {
return nil, fmt.Errorf("db_query: columns: %w", err)
}
colTypes, err := rows.ColumnTypes()
if err != nil {
return nil, fmt.Errorf("db_query: column types: %w", err)
}
var results []map[string]any
for rows.Next() {
// Use RawBytes so we can inspect the raw value before converting.
raw := make([]sql.RawBytes, len(cols))
ptrs := make([]any, len(cols))
for i := range raw {
ptrs[i] = &raw[i]
}
if err := rows.Scan(ptrs...); err != nil {
return nil, fmt.Errorf("db_query: scan: %w", err)
}
row := make(map[string]any, len(cols))
for i, col := range cols {
if raw[i] == nil {
row[col] = nil
continue
}
row[col] = convertRaw(raw[i], colTypes[i].DatabaseTypeName())
}
results = append(results, row)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("db_query: rows: %w", err)
}
return results, nil
}
// convertRaw attempts to convert a raw SQL byte slice into a native Go type
// based on the database column type name hint.
func convertRaw(b sql.RawBytes, dbType string) any {
s := string(b)
switch dbType {
case "INTEGER", "INT", "BIGINT", "SMALLINT", "TINYINT", "INT2", "INT4", "INT8":
if v, err := strconv.ParseInt(s, 10, 64); err == nil {
return v
}
case "REAL", "FLOAT", "DOUBLE", "NUMERIC", "DECIMAL":
if v, err := strconv.ParseFloat(s, 64); err == nil {
return v
}
case "BOOLEAN", "BOOL":
if v, err := strconv.ParseBool(s); err == nil {
return v
}
case "BLOB":
cp := make([]byte, len(b))
copy(cp, b)
return cp
}
return s
}
+37
View File
@@ -0,0 +1,37 @@
---
name: db_query
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func DBQuery(db *sql.DB, query string, args ...any) ([]map[string]any, error)"
description: "Ejecuta un SELECT y retorna los resultados como slice de maps. Convierte valores a tipos nativos Go segun el tipo de columna reportado por el driver."
tags: [database, sql, query, select, generic]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["database/sql"]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/db_query.go"
---
## Ejemplo
```go
rows, err := DBQuery(db, "SELECT id, name, score FROM players WHERE active = ?", true)
if err != nil {
return err
}
for _, row := range rows {
fmt.Println(row["name"], row["score"])
}
```
## Notas
Agnóstica al driver — funciona con cualquier `*sql.DB` (sqlite, duckdb, postgres, clickhouse). Usa `sql.RawBytes` + `ColumnTypes()` para conversion dinamica. Convierte INTEGER→int64, FLOAT/REAL/DOUBLE→float64, BOOLEAN→bool, BLOB→[]byte, NULL→nil, resto→string. Para queries con muchos resultados considerar paginar con LIMIT/OFFSET.
+27
View File
@@ -0,0 +1,27 @@
package infra
import (
"database/sql"
"fmt"
_ "github.com/marcboeker/go-duckdb"
)
// DuckDBOpen opens (or creates) a DuckDB database file.
// Pass an empty path or ":memory:" for an in-memory database.
// Returns a ready-to-use *sql.DB or an error.
func DuckDBOpen(path string) (*sql.DB, error) {
dsn := path
if dsn == "" {
dsn = ":memory:"
}
db, err := sql.Open("duckdb", dsn)
if err != nil {
return nil, fmt.Errorf("duckdb_open: open %q: %w", dsn, err)
}
if err := db.Ping(); err != nil {
db.Close()
return nil, fmt.Errorf("duckdb_open: ping %q: %w", dsn, err)
}
return db, nil
}
+38
View File
@@ -0,0 +1,38 @@
---
name: duckdb_open
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func DuckDBOpen(path string) (*sql.DB, error)"
description: "Abre (o crea) una base de datos DuckDB. Path vacio o ':memory:' abre una base en memoria."
tags: [database, duckdb, connection, sql, analytics]
uses_functions: []
uses_types: [db_config_go_infra]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["database/sql", "github.com/marcboeker/go-duckdb"]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/duckdb_open.go"
---
## Ejemplo
```go
// In-memory para analisis temporal
db, err := DuckDBOpen("")
if err != nil {
log.Fatal(err)
}
defer DBClose(db)
rows, err := DBQuery(db, "SELECT * FROM read_parquet('/data/sales.parquet')")
```
## Notas
Usa el driver `github.com/marcboeker/go-duckdb` (CGO). DuckDB es una base de datos OLAP embebida, ideal para analisis de datos. Path vacio equivale a `:memory:`. Hace ping al abrir para detectar errores temprano.
+32
View File
@@ -0,0 +1,32 @@
package infra
import (
"database/sql"
"fmt"
_ "github.com/jackc/pgx/v5/stdlib"
)
// PostgresOpen connects to a PostgreSQL server and returns a *sql.DB.
// sslmode defaults to "disable" when empty.
// Constructs a DSN of the form:
//
// host=<host> port=<port> user=<user> password=<password> dbname=<dbname> sslmode=<sslmode>
func PostgresOpen(host string, port int, user, password, dbname string, sslmode string) (*sql.DB, error) {
if sslmode == "" {
sslmode = "disable"
}
dsn := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
host, port, user, password, dbname, sslmode,
)
db, err := sql.Open("pgx", dsn)
if err != nil {
return nil, fmt.Errorf("postgres_open: open: %w", err)
}
if err := db.Ping(); err != nil {
db.Close()
return nil, fmt.Errorf("postgres_open: ping %s:%d/%s: %w", host, port, dbname, err)
}
return db, nil
}
+37
View File
@@ -0,0 +1,37 @@
---
name: postgres_open
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func PostgresOpen(host string, port int, user, password, dbname string, sslmode string) (*sql.DB, error)"
description: "Conecta a PostgreSQL construyendo el DSN desde parametros individuales. sslmode por defecto 'disable' si vacio."
tags: [database, postgres, postgresql, connection, sql]
uses_functions: []
uses_types: [db_config_go_infra]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["database/sql", "github.com/jackc/pgx/v5/stdlib"]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/postgres_open.go"
---
## Ejemplo
```go
db, err := PostgresOpen("localhost", 5432, "user", "secret", "mydb", "disable")
if err != nil {
log.Fatal(err)
}
defer DBClose(db)
rows, err := DBQuery(db, "SELECT id, name FROM users WHERE active = $1", true)
```
## Notas
Usa el driver `github.com/jackc/pgx/v5/stdlib` registrado como "pgx". Construye DSN con los parametros separados para mayor legibilidad. Para produccion usar `sslmode=require` o `sslmode=verify-full`. Hace ping al abrir para verificar conectividad.
+27
View File
@@ -0,0 +1,27 @@
package infra
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
// SQLiteOpen opens (or creates) a SQLite database file with WAL mode and
// foreign key support enabled. Returns a ready-to-use *sql.DB or an error.
// Pass ":memory:" for an in-memory database.
func SQLiteOpen(path string) (*sql.DB, error) {
if path == "" {
return nil, fmt.Errorf("sqlite_open: path must not be empty (use ':memory:' for in-memory)")
}
dsn := fmt.Sprintf("file:%s?_journal_mode=WAL&_foreign_keys=on", path)
db, err := sql.Open("sqlite3", dsn)
if err != nil {
return nil, fmt.Errorf("sqlite_open: open %q: %w", path, err)
}
if err := db.Ping(); err != nil {
db.Close()
return nil, fmt.Errorf("sqlite_open: ping %q: %w", path, err)
}
return db, nil
}
+37
View File
@@ -0,0 +1,37 @@
---
name: sqlite_open
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func SQLiteOpen(path string) (*sql.DB, error)"
description: "Abre (o crea) una base de datos SQLite con WAL mode y foreign keys habilitados. Hace ping para verificar la conexion."
tags: [database, sqlite, connection, sql]
uses_functions: []
uses_types: [db_config_go_infra]
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["database/sql", "github.com/mattn/go-sqlite3"]
tested: false
tests: []
test_file_path: ""
file_path: "functions/infra/sqlite_open.go"
---
## Ejemplo
```go
db, err := SQLiteOpen("/data/myapp.db")
if err != nil {
log.Fatal(err)
}
defer DBClose(db)
rows, err := DBQuery(db, "SELECT * FROM users WHERE active = ?", 1)
```
## Notas
Usa el driver `github.com/mattn/go-sqlite3` (CGO). El DSN incluye `_journal_mode=WAL` para mejor concurrencia y `_foreign_keys=on`. Acepta `:memory:` para base de datos en memoria. Hace ping al abrir para detectar errores temprano.
+21
View File
@@ -0,0 +1,21 @@
---
name: db_config
lang: go
domain: infra
version: "1.0.0"
algebraic: product
definition: |
type DBConfig struct {
Driver string // "sqlite", "duckdb", "postgres", "clickhouse"
DSN string // Data source name / connection string
Opts map[string]string // Driver-specific options (optional)
}
description: "Parametros de conexion para cualquier base de datos soportada. Agnóstico al driver."
tags: [database, config, connection, sqlite, duckdb, postgres, clickhouse]
uses_types: []
file_path: "functions/infra/db_config.go"
---
## Notas
Tipo producto — todos los campos siempre presentes. Driver toma uno de los valores: "sqlite", "duckdb", "postgres", "clickhouse". DSN es el connection string nativo del driver. Opts permite pasar opciones adicionales especificas del driver sin necesidad de un tipo por driver.