Files
base/core/dialect_postgres.go
T
zeekay 85387a93ab feat(base): add PostgreSQL dialect and DBConnect
SQLDialect interface abstracts SQLite vs PostgreSQL SQL differences:
- JSON ops (json_extract vs ->>, json_each vs jsonb_array_elements)
- Date formatting (strftime vs to_char)
- Schema introspection (PRAGMA vs information_schema)
- ID generation (randomblob vs gen_random_bytes)

Adds PostgresDBConnect via pgx driver and DataDSN/AuxDSN config fields
for connecting Base to Neon serverless PostgreSQL instead of SQLite.
2026-03-01 18:37:50 -08:00

156 lines
4.1 KiB
Go

package core
import (
"fmt"
"strings"
)
// PostgresDialect implements SQLDialect for PostgreSQL databases.
type PostgresDialect struct{}
func (d *PostgresDialect) DriverName() string {
return "pgx"
}
func (d *PostgresDialect) Placeholder(n int) string {
return fmt.Sprintf("$%d", n)
}
func (d *PostgresDialect) AutoincrementType() string {
return "BIGSERIAL PRIMARY KEY"
}
func (d *PostgresDialect) JSONExtract(column, path string) string {
// Strip leading "$." or "$" prefix used by SQLite JSON paths.
p := path
if strings.HasPrefix(p, "$.") {
p = p[2:]
} else if strings.HasPrefix(p, "$") {
p = p[1:]
}
// Handle nested paths like "a.b.c" -> col #>> '{a,b,c}'
parts := strings.Split(p, ".")
if len(parts) == 1 {
return fmt.Sprintf("%s->>'%s'", column, parts[0])
}
return fmt.Sprintf("%s #>> '{%s}'", column, strings.Join(parts, ","))
}
func (d *PostgresDialect) JSONEach(column string) string {
return fmt.Sprintf("jsonb_array_elements(%s)", column)
}
func (d *PostgresDialect) JSONValid(column string) string {
return fmt.Sprintf("(%s)::jsonb IS NOT NULL", column)
}
func (d *PostgresDialect) JSONArray(values ...string) string {
return fmt.Sprintf("jsonb_build_array(%s)", strings.Join(values, ", "))
}
// sqliteToPostgresFormat maps SQLite strftime format specifiers to PostgreSQL to_char patterns.
var sqliteToPostgresFormat = map[string]string{
"%Y": "YYYY",
"%m": "MM",
"%d": "DD",
"%H": "HH24",
"%M": "MI",
"%S": "SS",
"%s": "epoch",
"%w": "D",
"%f": "SS.US",
}
func (d *PostgresDialect) Strftime(format, column string) string {
// Handle epoch specially: extract epoch returns a number, not a formatted string.
if format == "%s" {
return fmt.Sprintf("EXTRACT(EPOCH FROM %s)", column)
}
pgFormat := format
for sqliteFmt, pgFmt := range sqliteToPostgresFormat {
pgFormat = strings.ReplaceAll(pgFormat, sqliteFmt, pgFmt)
}
// Handle the datetime format: %Y-%m-%d %H:%M:%fZ
// After replacement this becomes: YYYY-MM-DD HH24:MI:SS.USZ
return fmt.Sprintf("to_char(%s, '%s')", column, pgFormat)
}
func (d *PostgresDialect) Now() string {
return "NOW()"
}
func (d *PostgresDialect) RandomID(byteLen int) string {
return fmt.Sprintf("encode(gen_random_bytes(%d), 'hex')", byteLen)
}
func (d *PostgresDialect) Concat(parts ...string) string {
return strings.Join(parts, " || ")
}
func (d *PostgresDialect) GroupConcat(column, separator string) string {
return fmt.Sprintf("string_agg(%s, '%s')", column, separator)
}
func (d *PostgresDialect) TableColumns(table string) string {
return fmt.Sprintf(
"SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = '%s' ORDER BY ordinal_position",
table,
)
}
func (d *PostgresDialect) HasTable(table string) string {
return fmt.Sprintf(
"SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '%s' LIMIT 1",
table,
)
}
func (d *PostgresDialect) ListTables() string {
return "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' ORDER BY table_name"
}
func (d *PostgresDialect) ListViews() string {
return "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'VIEW' ORDER BY table_name"
}
// pgTypeMap maps SQLite type names to PostgreSQL equivalents.
var pgTypeMap = map[string]string{
"INTEGER": "BIGINT",
"REAL": "DOUBLE PRECISION",
"BLOB": "BYTEA",
"JSON": "JSONB",
// These map to themselves but are listed for clarity.
"TEXT": "TEXT",
"BOOLEAN": "BOOLEAN",
}
func (d *PostgresDialect) NormalizeColumnType(sqliteType string) string {
upper := strings.ToUpper(strings.TrimSpace(sqliteType))
if pgType, ok := pgTypeMap[upper]; ok {
return pgType
}
return sqliteType
}
func (d *PostgresDialect) Vacuum() string {
return "VACUUM"
}
func (d *PostgresDialect) BooleanValue(b bool) string {
if b {
return "TRUE"
}
return "FALSE"
}
func (d *PostgresDialect) SupportsReturning() bool {
return true
}
func (d *PostgresDialect) CastToText(column string) string {
return fmt.Sprintf("CAST(%s AS TEXT)", column)
}