nidus-sync/db/prepared.go

539 lines
14 KiB
Go
Raw Permalink Normal View History

package db
import (
"context"
"embed"
"encoding/json"
"fmt"
"path/filepath"
"strings"
"time"
//"github.com/stephenafamo/bob"
//"github.com/stephenafamo/bob/dialect/psql"
fslayer "github.com/Gleipnir-Technology/arcgis-go/fieldseeker/layer"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/psql"
"github.com/stephenafamo/scan"
)
//go:embed prepared_functions/*.sql
var sqlFiles embed.FS
// PrepareStatements reads all embedded SQL files and executes them
// against the provided database connection. This is intended for
// preparing statements that will be used later.
func prepareStatements(ctx context.Context) error {
return nil
// Get a list of all embedded SQL files
entries, err := sqlFiles.ReadDir("prepared_functions")
if err != nil {
return fmt.Errorf("failed to read SQL directory: %w", err)
}
log.Info().Int("len", len(entries)).Msg("Reading prepared functions")
// Process each SQL file
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") {
log.Info().Str("name", entry.Name()).Msg("Skipping")
continue
}
// Read the SQL file content
content, err := sqlFiles.ReadFile(filepath.Join("prepared_functions", entry.Name()))
if err != nil {
return fmt.Errorf("failed to read SQL file %s: %w", entry.Name(), err)
}
// Get the statement name from the filename (without extension)
statementName := strings.TrimSuffix(filepath.Base(entry.Name()), ".sql")
// Execute the SQL to prepare the statement
_, err = PGInstance.BobDB.Exec(string(content))
if err != nil {
return fmt.Errorf("failed to prepare statement %s: %w", statementName, err)
}
/*
query := psql.RawQuery(string(content))
stmt, err := bob.Prepare(ctx, PGInstance.BobDB, query)
if err != nil {
return fmt.Errorf("failed to prepare statement %s: %w", statementName, err)
}
*/
log.Info().Str("statement", statementName).Msg("Prepared statement")
}
return nil
}
func TestPreparedQueryOld(ctx context.Context) error {
type Skn struct {
Result int
}
q := fmt.Sprintf("EXECUTE test_function(%d)", 4)
query := psql.RawQuery(q)
result, err := bob.One[Skn](ctx, PGInstance.BobDB, query, scan.StructMapper[Skn]())
if err != nil {
return fmt.Errorf("Failed to exectue test function: %w", err)
}
log.Info().Int("value", result.Result).Msg("got result")
return nil
}
func TestPreparedQuery(ctx context.Context, row *fslayer.RodentLocation) error {
q := queryStoredProcedure("fieldseeker.insert_rodentlocation",
Uint("p_objectid", row.ObjectID),
String("p_locationname", row.LocationName),
String("p_zone", row.Zone),
String("p_zone2", row.Zone2),
String("p_habitat", row.Habitat),
String("p_priority", row.Priority),
String("p_usetype", row.Usetype),
Int16("p_active", row.Active),
String("p_description", row.Description),
String("p_accessdesc", row.Accessdesc),
String("p_comments", row.Comments),
String("p_symbology", row.Symbology),
String("p_externalid", row.ExternalID),
Timestamp("p_nextactiondatescheduled", row.Nextactiondatescheduled),
Int32("p_locationnumber", row.Locationnumber),
Timestamp("p_lastinspectdate", row.LastInspectionDate),
String("p_lastinspectspecies", row.LastInspectionSpecies),
String("p_lastinspectaction", row.LastInspectionAction),
String("p_lastinspectconditions", row.LastInspectionConditions),
String("p_lastinspectrodentevidence", row.LastInspectionRodentEvidence),
UUID("p_globalid", row.GlobalID),
String("p_created_user", row.CreatedUser),
Timestamp("p_created_date", row.CreatedDate),
String("p_last_edited_user", row.LastEditedUser),
Timestamp("p_last_edited_date", row.LastEditedDate),
Timestamp("p_creationdate", row.CreationDate),
String("p_creator", row.Creator),
Timestamp("p_editdate", row.EditDate),
String("p_editor", row.Editor),
String("p_jurisdiction", row.Jurisdiction),
)
query := psql.RawQuery(q)
log.Info().Str("query", q).Msg("querying")
2025-12-03 21:52:43 +00:00
result, err := bob.One[InsertResultRow](ctx, PGInstance.BobDB, query, scan.StructMapper[InsertResultRow]())
if err != nil {
return fmt.Errorf("Failed to execute test function: %w", err)
}
//log.Info().Int("version", result.NextVersion).Msg("got result")
//log.Info().Bool("added", result.Row.Added).Int("version", result.Row.Version).Msg("done")
log.Info().Bool("inserted", result.Inserted).Int("version", result.Version).Msg("done")
return nil
}
// SqlParam is a generic struct that wraps a parameter with its SQL representation
type SqlParam interface {
ToSql() string
}
type JsonBParam struct {
Name string
Value json.RawMessage
}
func (p JsonBParam) ToSql() string {
if len(p.Value) == 0 {
return fmt.Sprintf("%s => '{}'::jsonb", p.Name)
}
return fmt.Sprintf("%s => '%s'::jsonb", p.Name, p.Value)
}
type GeometryLine struct {
Paths [][][]float64 `json:"paths"`
}
type GISLineParam struct {
Name string
Value GeometryLine
WKID int
}
func (p GISLineParam) ToSql() string {
pairs := make([]string, 0)
if len(p.Value.Paths) > 1 {
log.Warn().Msg("Looks like we need to implement multi-path lines")
}
for _, path := range p.Value.Paths {
for _, pair := range path {
if len(pair) != 2 {
log.Warn().Int("len", len(pair)).Msg("Too many coords in line point")
continue
}
pairs = append(pairs, fmt.Sprintf("%f %f", pair[0], pair[1]))
}
}
var b strings.Builder
for i, p := range pairs {
b.WriteString(p)
if i < len(pairs)-1 {
b.WriteString(", ")
}
}
// Not sure why this isn't working, postgres is complaining that "function x is not uinque" and that
// I may want to add explicit casts...
//return fmt.Sprintf("%s => ST_SetSRID('LINESTRING(%s)', %d)::geometry(LineString,%d)", p.Name, b.String(), p.WKID, p.WKID)
return fmt.Sprintf("%s => 'LINESTRING(%s)'::geometry", p.Name, b.String())
}
type GeometryPoint struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}
type GISPointParam struct {
Name string
Value GeometryPoint
WKID int
}
func (p GISPointParam) ToSql() string {
return fmt.Sprintf("%s => ST_SetSRID(ST_MakePoint(%f, %f), %d)", p.Name, p.Value.X, p.Value.Y, p.WKID)
}
type GeometryPolygon struct {
Rings [][][]float64 `json:"rings"`
}
type GISPolygonParam struct {
Name string
Value GeometryPolygon
WKID int
}
func (p GISPolygonParam) ToSql() string {
pairs := make([]string, 0)
if len(p.Value.Rings) > 1 {
log.Warn().Msg("Looks like we need to implement multi-path lines")
}
for _, path := range p.Value.Rings {
for _, pair := range path {
if len(pair) != 2 {
log.Warn().Int("len", len(pair)).Msg("Too many coords in line point")
continue
}
pairs = append(pairs, fmt.Sprintf("%f %f", pair[0], pair[1]))
}
}
var b strings.Builder
for i, p := range pairs {
b.WriteString(p)
if i < len(pairs)-1 {
b.WriteString(", ")
}
}
// Not sure why this isn't working, postgres is complaining that "function x is not uinque" and that
// I may want to add explicit casts...
//return fmt.Sprintf("%s => ST_SetSRID('LINESTRING(%s)', %d)::geometry(LineString,%d)", p.Name, b.String(), p.WKID, p.WKID)
return fmt.Sprintf("%s => ST_MakePolygon('LINESTRING(%s)')::geometry", p.Name, b.String())
}
// StringParam wraps a string parameter
type StringParam struct {
Name string
Value string
}
func (p StringParam) ToSql() string {
// Escape quotes since we are writing text directly into the SQL query and this is a key delimiter
escapedQuotes := strings.ReplaceAll(string(p.Value), "'", "''")
// Escape question marks because they are a special signal for replacement to bob
escapedQuestions := strings.ReplaceAll(escapedQuotes, "?", "\\?")
return fmt.Sprintf("%s => '%s'::varchar", p.Name, escapedQuestions)
}
type Float64Param struct {
Name string
Value float64
}
func (p Float64Param) ToSql() string {
return fmt.Sprintf("%s => %f::double precision", p.Name, p.Value)
}
// IntParam wraps an int parameter
type Int16Param struct {
Name string
Value int16
}
func (p Int16Param) ToSql() string {
return fmt.Sprintf("%s => %d::smallint", p.Name, p.Value)
}
type Int32Param struct {
Name string
Value int32
}
func (p Int32Param) ToSql() string {
return fmt.Sprintf("%s => %d::int", p.Name, p.Value)
}
type Int64Param struct {
Name string
Value int64
}
func (p Int64Param) ToSql() string {
return fmt.Sprintf("%s => %d::bigint", p.Name, p.Value)
}
// UintParam wraps a uint parameter
type UintParam struct {
Name string
Value uint
}
func (p UintParam) ToSql() string {
return fmt.Sprintf("%s => %d::int", p.Name, p.Value)
}
type Uint32Param struct {
Name string
Value uint32
}
func (p Uint32Param) ToSql() string {
return fmt.Sprintf("%s => %d::int", p.Name, p.Value)
}
type Uint64Param struct {
Name string
Value uint64
}
func (p Uint64Param) ToSql() string {
return fmt.Sprintf("%s => %d::bigint", p.Name, p.Value)
}
type UUIDParam struct {
Name string
Value string
}
func (p UUIDParam) ToSql() string {
return fmt.Sprintf("%s => '%s'", p.Name, p.Value)
}
// FloatParam wraps a float parameter
type FloatParam float64
func (p FloatParam) ToSql() string {
return fmt.Sprintf("%f", p)
}
// BoolParam wraps a boolean parameter
type BoolParam bool
func (p BoolParam) ToSql() string {
return fmt.Sprintf("%t", p)
}
// EnumParam wraps a PostgreSQL enum parameter
type EnumParam string
func (p EnumParam) ToSql() string {
escapedStr := strings.ReplaceAll(string(p), "'", "''")
return fmt.Sprintf("'%s'", escapedStr)
}
// NullParam represents a NULL value
type NullParam struct {
Name string
}
func (p NullParam) ToSql() string {
return fmt.Sprintf("%s => NULL", p.Name)
}
// Convenience functions to create typed parameters
func String(n, s string) StringParam {
return StringParam{
Name: n,
Value: s,
}
}
type Stringable interface {
String() string
}
func Enum(n string, e Stringable) EnumParam {
return EnumParam(e.String())
}
func Float64(n string, f float64) Float64Param {
return Float64Param{n, f}
}
func GISLine(n string, v GeometryLine, wkid int) GISLineParam {
return GISLineParam{n, v, wkid}
}
func GISPoint(n string, v GeometryPoint, wkid int) GISPointParam {
return GISPointParam{n, v, wkid}
}
func GISPolygon(n string, v GeometryPolygon, wkid int) GISPolygonParam {
return GISPolygonParam{n, v, wkid}
}
func Int16(n string, i int16) Int16Param {
return Int16Param{n, i}
}
func Int32(n string, i int32) Int32Param {
return Int32Param{n, i}
}
func Int64(n string, i int64) Int64Param {
return Int64Param{n, i}
}
func JsonB(n string, v json.RawMessage) JsonBParam {
return JsonBParam{n, v}
}
// Timestamp creates a PostgreSQL TIMESTAMP WITHOUT TIME ZONE parameter
func Timestamp(name string, t time.Time) TimestampParam {
return TimestampParam{name, t}
}
// Timestamptz creates a PostgreSQL TIMESTAMP WITH TIME ZONE parameter
func Timestamptz(t time.Time) TimestamptzParam {
return TimestamptzParam(t)
}
func Uint(name string, u uint) UintParam {
return UintParam{name, u}
}
func Uint32(name string, u uint) Uint32Param {
return Uint32Param{name, uint32(u)}
}
func Uint64(name string, u uint64) Uint64Param {
return Uint64Param{name, u}
}
func UUID(name string, u uuid.UUID) UUIDParam {
return UUIDParam{name, u.String()}
}
func Float(f float64) FloatParam {
return FloatParam(f)
}
func Bool(b bool) BoolParam {
return BoolParam(b)
}
func Null() NullParam {
return NullParam{}
}
// TimestampParam wraps a time.Time parameter for PostgreSQL TIMESTAMP WITHOUT TIME ZONE
type TimestampParam struct {
Name string
Value time.Time
}
func (p TimestampParam) ToSql() string {
// Format as PostgreSQL timestamp without timezone
// The format string is based on PostgreSQL's expected format
t := time.Time(p.Value)
return fmt.Sprintf("%s => '%s'::timestamp", p.Name, t.Format("2006-01-02 15:04:05.999999"))
}
// TimestamptzParam wraps a time.Time parameter for PostgreSQL TIMESTAMP WITH TIME ZONE
type TimestamptzParam time.Time
func (p TimestamptzParam) ToSql() string {
// Format as PostgreSQL timestamp with timezone
t := time.Time(p)
return fmt.Sprintf("'%s'::timestamptz", t.Format("2006-01-02 15:04:05.999999-07:00"))
}
func queryStoredProcedure(procedure string, params ...SqlParam) string {
if len(params) == 0 {
2025-12-03 21:52:43 +00:00
return fmt.Sprintf("SELECT * FROM %s()", procedure)
}
// Convert each parameter to its SQL representation
paramStrings := make([]string, len(params))
for i, param := range params {
paramStrings[i] = param.ToSql()
}
// Join parameters and return the execute statement
2025-12-03 21:52:43 +00:00
return fmt.Sprintf("SELECT * FROM %s(%s)", procedure, strings.Join(paramStrings, ", "))
}
func executeFunction(functionName string, params ...SqlParam) string {
if len(params) == 0 {
return fmt.Sprintf("EXECUTE %s()", functionName)
}
// Convert each parameter to its SQL representation
paramStrings := make([]string, len(params))
for i, param := range params {
paramStrings[i] = param.ToSql()
}
// Join parameters and return the execute statement
return fmt.Sprintf("EXECUTE %s(%s)", functionName, strings.Join(paramStrings, ", "))
}
func parseLine(msg json.RawMessage) (result GeometryLine, err error) {
err = json.Unmarshal(msg, &result)
if err != nil {
return result, fmt.Errorf("Failed to parse line from '%s': %w", string(msg), err)
}
return result, nil
}
func parsePoint(msg json.RawMessage) (result GeometryPoint, err error) {
err = json.Unmarshal(msg, &result)
if err != nil {
return result, fmt.Errorf("Failed to parse point from '%s': %w", string(msg), err)
}
return result, nil
}
func parsePolygon(msg json.RawMessage) (result GeometryPolygon, err error) {
err = json.Unmarshal(msg, &result)
if err != nil {
return result, fmt.Errorf("Failed to parse polygon from '%s': %w", string(msg), err)
}
return result, nil
}
func lineOrNull(msg json.RawMessage) (SqlParam, error) {
// Surprisingly some geos are actually empty
if len(msg) == 0 {
return NullParam{"p_geospatial"}, nil
}
geo, err := parseLine(msg)
if err != nil {
return NullParam{"p_geospatial"}, fmt.Errorf("Failed to pepare GISLine: %w", err)
}
return GISLine("p_geospatial", geo, 3857), nil
}
func pointOrNull(msg json.RawMessage) (SqlParam, error) {
// Surprisingly some geos are actually empty
if len(msg) == 0 {
return NullParam{"p_geospatial"}, nil
}
geo, err := parsePoint(msg)
if err != nil {
return NullParam{"p_geospatial"}, fmt.Errorf("Failed to pepare GISPoint: %w", err)
}
return GISPoint("p_geospatial", geo, 3857), nil
}
func polygonOrNull(msg json.RawMessage) (SqlParam, error) {
// Surprisingly some geos are actually empty
if len(msg) == 0 {
return NullParam{"p_geospatial"}, nil
}
geo, err := parsePolygon(msg)
if err != nil {
return NullParam{"p_geospatial"}, fmt.Errorf("Failed to pepare GISPolygon: %w", err)
}
return GISPolygon("p_geospatial", geo, 3857), nil
}