From 47235e702cd517703fb03f9c9e4e9ec0359b3102 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Wed, 1 Apr 2026 20:55:17 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20abstracci=C3=B3n=20DB=20multi-engine=20?= =?UTF-8?q?=E2=80=94=20CRUD=20gen=C3=A9rico=20y=20openers=20para=20SQLite,?= =?UTF-8?q?=20Postgres,=20ClickHouse,=20DuckDB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- functions/infra/clickhouse_open.go | 25 ++++++++++ functions/infra/clickhouse_open.md | 37 ++++++++++++++ functions/infra/db_close.go | 18 +++++++ functions/infra/db_close.md | 35 +++++++++++++ functions/infra/db_config.go | 8 +++ functions/infra/db_create_table.go | 32 ++++++++++++ functions/infra/db_create_table.md | 36 ++++++++++++++ functions/infra/db_exec.go | 22 +++++++++ functions/infra/db_exec.md | 35 +++++++++++++ functions/infra/db_insert_batch.go | 70 ++++++++++++++++++++++++++ functions/infra/db_insert_batch.md | 41 ++++++++++++++++ functions/infra/db_insert_row.go | 54 ++++++++++++++++++++ functions/infra/db_insert_row.md | 39 +++++++++++++++ functions/infra/db_query.go | 79 ++++++++++++++++++++++++++++++ functions/infra/db_query.md | 37 ++++++++++++++ functions/infra/duckdb_open.go | 27 ++++++++++ functions/infra/duckdb_open.md | 38 ++++++++++++++ functions/infra/postgres_open.go | 32 ++++++++++++ functions/infra/postgres_open.md | 37 ++++++++++++++ functions/infra/sqlite_open.go | 27 ++++++++++ functions/infra/sqlite_open.md | 37 ++++++++++++++ types/infra/db_config.md | 21 ++++++++ 22 files changed, 787 insertions(+) create mode 100644 functions/infra/clickhouse_open.go create mode 100644 functions/infra/clickhouse_open.md create mode 100644 functions/infra/db_close.go create mode 100644 functions/infra/db_close.md create mode 100644 functions/infra/db_config.go create mode 100644 functions/infra/db_create_table.go create mode 100644 functions/infra/db_create_table.md create mode 100644 functions/infra/db_exec.go create mode 100644 functions/infra/db_exec.md create mode 100644 functions/infra/db_insert_batch.go create mode 100644 functions/infra/db_insert_batch.md create mode 100644 functions/infra/db_insert_row.go create mode 100644 functions/infra/db_insert_row.md create mode 100644 functions/infra/db_query.go create mode 100644 functions/infra/db_query.md create mode 100644 functions/infra/duckdb_open.go create mode 100644 functions/infra/duckdb_open.md create mode 100644 functions/infra/postgres_open.go create mode 100644 functions/infra/postgres_open.md create mode 100644 functions/infra/sqlite_open.go create mode 100644 functions/infra/sqlite_open.md create mode 100644 types/infra/db_config.md diff --git a/functions/infra/clickhouse_open.go b/functions/infra/clickhouse_open.go new file mode 100644 index 00000000..8d69e4c4 --- /dev/null +++ b/functions/infra/clickhouse_open.go @@ -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 +} diff --git a/functions/infra/clickhouse_open.md b/functions/infra/clickhouse_open.md new file mode 100644 index 00000000..380968a8 --- /dev/null +++ b/functions/infra/clickhouse_open.md @@ -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. diff --git a/functions/infra/db_close.go b/functions/infra/db_close.go new file mode 100644 index 00000000..da0611a8 --- /dev/null +++ b/functions/infra/db_close.go @@ -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 +} diff --git a/functions/infra/db_close.md b/functions/infra/db_close.md new file mode 100644 index 00000000..8ce8c5d7 --- /dev/null +++ b/functions/infra/db_close.md @@ -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. diff --git a/functions/infra/db_config.go b/functions/infra/db_config.go new file mode 100644 index 00000000..d87608d0 --- /dev/null +++ b/functions/infra/db_config.go @@ -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) +} diff --git a/functions/infra/db_create_table.go b/functions/infra/db_create_table.go new file mode 100644 index 00000000..55640436 --- /dev/null +++ b/functions/infra/db_create_table.go @@ -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 +} diff --git a/functions/infra/db_create_table.md b/functions/infra/db_create_table.md new file mode 100644 index 00000000..b4430439 --- /dev/null +++ b/functions/infra/db_create_table.md @@ -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. diff --git a/functions/infra/db_exec.go b/functions/infra/db_exec.go new file mode 100644 index 00000000..1f0ca6c2 --- /dev/null +++ b/functions/infra/db_exec.go @@ -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 +} diff --git a/functions/infra/db_exec.md b/functions/infra/db_exec.md new file mode 100644 index 00000000..19288698 --- /dev/null +++ b/functions/infra/db_exec.md @@ -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`. diff --git a/functions/infra/db_insert_batch.go b/functions/infra/db_insert_batch.go new file mode 100644 index 00000000..99df7bfa --- /dev/null +++ b/functions/infra/db_insert_batch.go @@ -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 +} diff --git a/functions/infra/db_insert_batch.md b/functions/infra/db_insert_batch.md new file mode 100644 index 00000000..4b91159a --- /dev/null +++ b/functions/infra/db_insert_batch.md @@ -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. diff --git a/functions/infra/db_insert_row.go b/functions/infra/db_insert_row.go new file mode 100644 index 00000000..ef05a934 --- /dev/null +++ b/functions/infra/db_insert_row.go @@ -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 +} diff --git a/functions/infra/db_insert_row.md b/functions/infra/db_insert_row.md new file mode 100644 index 00000000..181c0c16 --- /dev/null +++ b/functions/infra/db_insert_row.md @@ -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`). diff --git a/functions/infra/db_query.go b/functions/infra/db_query.go new file mode 100644 index 00000000..e134ac73 --- /dev/null +++ b/functions/infra/db_query.go @@ -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 +} diff --git a/functions/infra/db_query.md b/functions/infra/db_query.md new file mode 100644 index 00000000..582ad209 --- /dev/null +++ b/functions/infra/db_query.md @@ -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. diff --git a/functions/infra/duckdb_open.go b/functions/infra/duckdb_open.go new file mode 100644 index 00000000..9a240a8c --- /dev/null +++ b/functions/infra/duckdb_open.go @@ -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 +} diff --git a/functions/infra/duckdb_open.md b/functions/infra/duckdb_open.md new file mode 100644 index 00000000..b8602c95 --- /dev/null +++ b/functions/infra/duckdb_open.md @@ -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. diff --git a/functions/infra/postgres_open.go b/functions/infra/postgres_open.go new file mode 100644 index 00000000..548ee491 --- /dev/null +++ b/functions/infra/postgres_open.go @@ -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= port= user= password= dbname= 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 +} diff --git a/functions/infra/postgres_open.md b/functions/infra/postgres_open.md new file mode 100644 index 00000000..f9c4f1f2 --- /dev/null +++ b/functions/infra/postgres_open.md @@ -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. diff --git a/functions/infra/sqlite_open.go b/functions/infra/sqlite_open.go new file mode 100644 index 00000000..cf894f6d --- /dev/null +++ b/functions/infra/sqlite_open.go @@ -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 +} diff --git a/functions/infra/sqlite_open.md b/functions/infra/sqlite_open.md new file mode 100644 index 00000000..bb9964c9 --- /dev/null +++ b/functions/infra/sqlite_open.md @@ -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. diff --git a/types/infra/db_config.md b/types/infra/db_config.md new file mode 100644 index 00000000..bc50f973 --- /dev/null +++ b/types/infra/db_config.md @@ -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.