feat: 🎉 postgresql broker first commit of forked prototype from my original code

This commit is contained in:
2026-01-02 20:56:39 +02:00
parent e90e5902cd
commit 19e469ff54
29 changed files with 3325 additions and 2 deletions

View File

@@ -0,0 +1,246 @@
package install
import (
"context"
"embed"
"fmt"
"io/fs"
"sort"
"strings"
"git.warky.dev/wdevs/pgsql-broker/pkg/broker/adapter"
)
//go:embed all:sql
var sqlFS embed.FS
// Installer handles database schema installation
type Installer struct {
db adapter.DBAdapter
logger adapter.Logger
}
// New creates a new installer
func New(db adapter.DBAdapter, logger adapter.Logger) *Installer {
return &Installer{
db: db,
logger: logger,
}
}
// InstallSchema installs the complete database schema
func (i *Installer) InstallSchema(ctx context.Context) error {
i.logger.Info("starting schema installation")
// Install tables first
if err := i.installTables(ctx); err != nil {
return fmt.Errorf("failed to install tables: %w", err)
}
// Then install procedures
if err := i.installProcedures(ctx); err != nil {
return fmt.Errorf("failed to install procedures: %w", err)
}
i.logger.Info("schema installation completed successfully")
return nil
}
// installTables installs all table definitions
func (i *Installer) installTables(ctx context.Context) error {
i.logger.Info("installing tables")
files, err := sqlFS.ReadDir("sql/tables")
if err != nil {
return fmt.Errorf("failed to read tables directory: %w", err)
}
// Filter and sort SQL files
sqlFiles := filterAndSortSQLFiles(files)
for _, file := range sqlFiles {
// Skip install script
if file == "00_install.sql" {
continue
}
i.logger.Info("executing table script", "file", file)
content, err := sqlFS.ReadFile("sql/tables/" + file)
if err != nil {
return fmt.Errorf("failed to read file %s: %w", file, err)
}
if err := i.executeSQL(ctx, string(content)); err != nil {
return fmt.Errorf("failed to execute %s: %w", file, err)
}
}
i.logger.Info("tables installed successfully")
return nil
}
// installProcedures installs all stored procedures
func (i *Installer) installProcedures(ctx context.Context) error {
i.logger.Info("installing procedures")
files, err := sqlFS.ReadDir("sql/procedures")
if err != nil {
return fmt.Errorf("failed to read procedures directory: %w", err)
}
// Filter and sort SQL files
sqlFiles := filterAndSortSQLFiles(files)
for _, file := range sqlFiles {
// Skip install script
if file == "00_install.sql" {
continue
}
i.logger.Info("executing procedure script", "file", file)
content, err := sqlFS.ReadFile("sql/procedures/" + file)
if err != nil {
return fmt.Errorf("failed to read file %s: %w", file, err)
}
if err := i.executeSQL(ctx, string(content)); err != nil {
return fmt.Errorf("failed to execute %s: %w", file, err)
}
}
i.logger.Info("procedures installed successfully")
return nil
}
// executeSQL executes SQL statements
func (i *Installer) executeSQL(ctx context.Context, sql string) error {
// Remove comments and split by statement
statements := splitSQLStatements(sql)
for _, stmt := range statements {
stmt = strings.TrimSpace(stmt)
if stmt == "" {
continue
}
// Skip psql-specific commands
if strings.HasPrefix(stmt, "\\") {
continue
}
if _, err := i.db.Exec(ctx, stmt); err != nil {
return fmt.Errorf("failed to execute statement: %w\nStatement: %s", err, stmt)
}
}
return nil
}
// filterAndSortSQLFiles filters and sorts SQL files
func filterAndSortSQLFiles(files []fs.DirEntry) []string {
var sqlFiles []string
for _, file := range files {
if !file.IsDir() && strings.HasSuffix(file.Name(), ".sql") {
sqlFiles = append(sqlFiles, file.Name())
}
}
sort.Strings(sqlFiles)
return sqlFiles
}
// splitSQLStatements splits SQL into individual statements
func splitSQLStatements(sql string) []string {
// Simple split by semicolon
// This doesn't handle all edge cases (strings with semicolons, dollar-quoted strings, etc.)
// but works for our use case
statements := strings.Split(sql, ";")
var result []string
var buffer string
for _, stmt := range statements {
stmt = strings.TrimSpace(stmt)
if stmt == "" {
continue
}
buffer += stmt + ";"
// Check if we're inside a function definition ($$)
dollarCount := strings.Count(buffer, "$$")
if dollarCount%2 == 0 {
// Even number of $$ means we're outside function definitions
result = append(result, buffer)
buffer = ""
} else {
// Odd number means we're inside a function, keep accumulating
buffer += " "
}
}
// Add any remaining buffered content
if buffer != "" {
result = append(result, buffer)
}
return result
}
// VerifyInstallation checks if the schema is properly installed
func (i *Installer) VerifyInstallation(ctx context.Context) error {
i.logger.Info("verifying installation")
tables := []string{"broker_queueinstance", "broker_jobs", "broker_schedule"}
procedures := []string{
"broker_get",
"broker_run",
"broker_set",
"broker_add_job",
"broker_register_instance",
"broker_ping_instance",
"broker_shutdown_instance",
}
// Check tables
for _, table := range tables {
var exists bool
err := i.db.QueryRow(ctx,
"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = $1)",
table,
).Scan(&exists)
if err != nil {
return fmt.Errorf("failed to check table %s: %w", table, err)
}
if !exists {
return fmt.Errorf("table %s does not exist", table)
}
i.logger.Info("table verified", "table", table)
}
// Check procedures
for _, proc := range procedures {
var exists bool
err := i.db.QueryRow(ctx,
"SELECT EXISTS (SELECT FROM pg_proc WHERE proname = $1)",
proc,
).Scan(&exists)
if err != nil {
return fmt.Errorf("failed to check procedure %s: %w", proc, err)
}
if !exists {
return fmt.Errorf("procedure %s does not exist", proc)
}
i.logger.Info("procedure verified", "procedure", proc)
}
i.logger.Info("installation verified successfully")
return nil
}