107 lines
3.0 KiB
Go
107 lines
3.0 KiB
Go
package assetloader
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// namedPlaceholder matches :identifier patterns (but not ::cast syntax).
|
|
var namedPlaceholder = regexp.MustCompile(`:([a-zA-Z_][a-zA-Z0-9_]*)`)
|
|
|
|
// pgCastMarker temporarily replaces :: to protect PostgreSQL cast syntax.
|
|
const pgCastMarker = "\x00PGCAST\x00"
|
|
|
|
// BuildQuery converts a SQL call that uses :name named placeholders into a
|
|
// pgx-compatible positional-parameter query ($1, $2, …) and returns the
|
|
// corresponding argument slice.
|
|
//
|
|
// Built-in placeholders:
|
|
// - :bytes → fileBytes ([]byte)
|
|
// - :filename → filename (string, base name only)
|
|
// - :any_key → staticParams["any_key"] (string)
|
|
//
|
|
// A placeholder that appears more than once maps to the same $N. An unknown
|
|
// placeholder (not built-in and not in staticParams) returns an error.
|
|
// PostgreSQL cast syntax (::type) is left untouched.
|
|
func BuildQuery(call string, fileBytes []byte, filename string, staticParams map[string]string) (query string, args []any, err error) {
|
|
// Protect :: casts before running the placeholder regex.
|
|
protected := strings.ReplaceAll(call, "::", pgCastMarker)
|
|
|
|
paramIndex := map[string]int{} // name → 1-based position
|
|
var firstErr error
|
|
|
|
query = namedPlaceholder.ReplaceAllStringFunc(protected, func(match string) string {
|
|
if firstErr != nil {
|
|
return match
|
|
}
|
|
name := match[1:] // strip leading ':'
|
|
|
|
// Return existing positional param for repeated placeholders.
|
|
if idx, seen := paramIndex[name]; seen {
|
|
return fmt.Sprintf("$%d", idx)
|
|
}
|
|
|
|
// Resolve the placeholder value.
|
|
var val any
|
|
switch name {
|
|
case "bytes":
|
|
val = fileBytes
|
|
case "filename":
|
|
val = filename
|
|
default:
|
|
if staticParams != nil {
|
|
if v, ok := staticParams[name]; ok {
|
|
val = v
|
|
}
|
|
}
|
|
if val == nil {
|
|
firstErr = fmt.Errorf("unknown placeholder %q in SQL call (not a built-in and not listed in params)", match)
|
|
return match
|
|
}
|
|
}
|
|
|
|
idx := len(args) + 1
|
|
paramIndex[name] = idx
|
|
args = append(args, val)
|
|
return fmt.Sprintf("$%d", idx)
|
|
})
|
|
|
|
if firstErr != nil {
|
|
return "", nil, firstErr
|
|
}
|
|
|
|
// Restore :: casts.
|
|
query = strings.ReplaceAll(query, pgCastMarker, "::")
|
|
|
|
return query, args, nil
|
|
}
|
|
|
|
// ExecuteItem reads the asset file referenced by item.Entry.File (which is the
|
|
// absolute path set by ScanDir) and executes the configured SQL call via conn.
|
|
// The file's raw bytes are bound as a []byte parameter — no encoding or escaping.
|
|
func ExecuteItem(ctx context.Context, conn *pgx.Conn, item Item) error {
|
|
data, err := os.ReadFile(item.Entry.File)
|
|
if err != nil {
|
|
return fmt.Errorf("reading asset file %s: %w", item.Entry.File, err)
|
|
}
|
|
|
|
filename := filepath.Base(item.Entry.File)
|
|
|
|
sql, args, err := BuildQuery(item.Entry.Call, data, filename, item.Entry.Params)
|
|
if err != nil {
|
|
return fmt.Errorf("building query for %s: %w", filename, err)
|
|
}
|
|
|
|
if _, err := conn.Exec(ctx, sql, args...); err != nil {
|
|
return fmt.Errorf("executing asset %s: %w", filename, err)
|
|
}
|
|
|
|
return nil
|
|
}
|