refactor: implement database-specific schema architecture with schema-as-query pattern
🏗️ **Major Database Schema Refactoring** **Problem Solved**: Eliminated model duplication and multiple sources of truth by: - Removed duplicate models (`internal/models/content.go`) - Replaced inlined schema strings with sqlc-generated setup functions - Implemented database-specific schemas with proper NOT NULL constraints **Key Improvements**: ✅ **Single Source of Truth**: Database schemas define all types, no manual sync needed ✅ **Clean Generated Types**: sqlc generates `string` and `int64` instead of `sql.NullString/sql.NullTime` ✅ **Schema-as-Query Pattern**: Setup functions generated by sqlc for type safety ✅ **Database-Specific Optimization**: SQLite INTEGER timestamps, PostgreSQL BIGINT timestamps ✅ **Cross-Database Compatibility**: Single codebase supports both SQLite and PostgreSQL **Architecture Changes**: - `db/sqlite/` - SQLite-specific schema and setup queries - `db/postgresql/` - PostgreSQL-specific schema and setup queries - `db/queries/` - Cross-database CRUD queries using `sqlc.arg()` syntax - `internal/db/database.go` - Database abstraction with runtime selection - `internal/api/models.go` - Clean API models for requests/responses **Version Control System**: Complete element-level history with user attribution and rollback **Verification**: ✅ Full API workflow tested (create → update → rollback → versions) **Production Ready**: Supports SQLite (development) → PostgreSQL (production) migration
This commit is contained in:
184
insertr-server/internal/db/database.go
Normal file
184
insertr-server/internal/db/database.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"github.com/insertr/server/internal/db/postgresql"
|
||||
"github.com/insertr/server/internal/db/sqlite"
|
||||
)
|
||||
|
||||
// Database wraps the database connection and queries
|
||||
type Database struct {
|
||||
conn *sql.DB
|
||||
dbType string
|
||||
|
||||
// Type-specific query interfaces
|
||||
sqliteQueries *sqlite.Queries
|
||||
postgresqlQueries *postgresql.Queries
|
||||
}
|
||||
|
||||
// NewDatabase creates a new database connection
|
||||
func NewDatabase(dbPath string) (*Database, error) {
|
||||
var conn *sql.DB
|
||||
var dbType string
|
||||
var err error
|
||||
|
||||
// Determine database type from connection string
|
||||
if strings.Contains(dbPath, "postgres://") || strings.Contains(dbPath, "postgresql://") {
|
||||
dbType = "postgresql"
|
||||
conn, err = sql.Open("postgres", dbPath)
|
||||
} else {
|
||||
dbType = "sqlite3"
|
||||
conn, err = sql.Open("sqlite3", dbPath)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
// Test connection
|
||||
if err := conn.Ping(); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
// Initialize the appropriate queries
|
||||
db := &Database{
|
||||
conn: conn,
|
||||
dbType: dbType,
|
||||
}
|
||||
|
||||
switch dbType {
|
||||
case "sqlite3":
|
||||
// Initialize SQLite schema using generated functions
|
||||
db.sqliteQueries = sqlite.New(conn)
|
||||
if err := db.initializeSQLiteSchema(); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to initialize SQLite schema: %w", err)
|
||||
}
|
||||
case "postgresql":
|
||||
// Initialize PostgreSQL schema using generated functions
|
||||
db.postgresqlQueries = postgresql.New(conn)
|
||||
if err := db.initializePostgreSQLSchema(); err != nil {
|
||||
conn.Close()
|
||||
return nil, fmt.Errorf("failed to initialize PostgreSQL schema: %w", err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database type: %s", dbType)
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func (db *Database) Close() error {
|
||||
return db.conn.Close()
|
||||
}
|
||||
|
||||
// GetQueries returns the appropriate query interface
|
||||
func (db *Database) GetSQLiteQueries() *sqlite.Queries {
|
||||
return db.sqliteQueries
|
||||
}
|
||||
|
||||
func (db *Database) GetPostgreSQLQueries() *postgresql.Queries {
|
||||
return db.postgresqlQueries
|
||||
}
|
||||
|
||||
// GetDBType returns the database type
|
||||
func (db *Database) GetDBType() string {
|
||||
return db.dbType
|
||||
}
|
||||
|
||||
// initializeSQLiteSchema sets up the SQLite database schema
|
||||
func (db *Database) initializeSQLiteSchema() error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create tables
|
||||
if err := db.sqliteQueries.InitializeSchema(ctx); err != nil {
|
||||
return fmt.Errorf("failed to create content table: %w", err)
|
||||
}
|
||||
|
||||
if err := db.sqliteQueries.InitializeVersionsTable(ctx); err != nil {
|
||||
return fmt.Errorf("failed to create content_versions table: %w", err)
|
||||
}
|
||||
|
||||
// Create indexes (manual for now since sqlc didn't generate them)
|
||||
indexQueries := []string{
|
||||
"CREATE INDEX IF NOT EXISTS idx_content_site_id ON content(site_id);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_content_updated_at ON content(updated_at);",
|
||||
"CREATE INDEX IF NOT EXISTS idx_content_versions_lookup ON content_versions(content_id, site_id, created_at DESC);",
|
||||
}
|
||||
|
||||
for _, query := range indexQueries {
|
||||
if _, err := db.conn.Exec(query); err != nil {
|
||||
return fmt.Errorf("failed to create index: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create update trigger (manual for now)
|
||||
triggerQuery := `
|
||||
CREATE TRIGGER IF NOT EXISTS update_content_updated_at
|
||||
AFTER UPDATE ON content
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE content SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id;
|
||||
END;`
|
||||
|
||||
if _, err := db.conn.Exec(triggerQuery); err != nil {
|
||||
return fmt.Errorf("failed to create update trigger: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initializePostgreSQLSchema sets up the PostgreSQL database schema
|
||||
func (db *Database) initializePostgreSQLSchema() error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create tables
|
||||
if err := db.postgresqlQueries.InitializeSchema(ctx); err != nil {
|
||||
return fmt.Errorf("failed to create content table: %w", err)
|
||||
}
|
||||
|
||||
if err := db.postgresqlQueries.InitializeVersionsTable(ctx); err != nil {
|
||||
return fmt.Errorf("failed to create content_versions table: %w", err)
|
||||
}
|
||||
|
||||
// Create indexes using generated functions
|
||||
if err := db.postgresqlQueries.CreateContentSiteIndex(ctx); err != nil {
|
||||
return fmt.Errorf("failed to create content site index: %w", err)
|
||||
}
|
||||
|
||||
if err := db.postgresqlQueries.CreateContentUpdatedAtIndex(ctx); err != nil {
|
||||
return fmt.Errorf("failed to create content updated_at index: %w", err)
|
||||
}
|
||||
|
||||
if err := db.postgresqlQueries.CreateVersionsLookupIndex(ctx); err != nil {
|
||||
return fmt.Errorf("failed to create versions lookup index: %w", err)
|
||||
}
|
||||
|
||||
// Create update function and trigger
|
||||
if err := db.postgresqlQueries.CreateUpdateFunction(ctx); err != nil {
|
||||
return fmt.Errorf("failed to create update function: %w", err)
|
||||
}
|
||||
|
||||
// Create trigger manually (sqlc didn't generate this)
|
||||
triggerQuery := `
|
||||
DROP TRIGGER IF EXISTS update_content_updated_at ON content;
|
||||
CREATE TRIGGER update_content_updated_at
|
||||
BEFORE UPDATE ON content
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_content_timestamp();`
|
||||
|
||||
if _, err := db.conn.Exec(triggerQuery); err != nil {
|
||||
return fmt.Errorf("failed to create update trigger: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user