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:
@@ -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
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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`.
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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`).
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user