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,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
|
||||
}
|
||||
Reference in New Issue
Block a user