package db import ( "context" "database/sql" "fmt" "strings" _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" "github.com/insertr/insertr/internal/db/postgresql" "github.com/insertr/insertr/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 } // GetSQLiteDB returns the underlying SQLite database connection func (db *Database) GetSQLiteDB() *sql.DB { return db.conn } // GetPostgreSQLDB returns the underlying PostgreSQL database connection func (db *Database) GetPostgreSQLDB() *sql.DB { return db.conn } // 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 collection tables if err := db.sqliteQueries.InitializeCollectionsTable(ctx); err != nil { return fmt.Errorf("failed to create collections table: %w", err) } if err := db.sqliteQueries.InitializeCollectionTemplatesTable(ctx); err != nil { return fmt.Errorf("failed to create collection_templates table: %w", err) } if err := db.sqliteQueries.InitializeCollectionItemsTable(ctx); err != nil { return fmt.Errorf("failed to create collection_items table: %w", err) } if err := db.sqliteQueries.InitializeCollectionItemVersionsTable(ctx); err != nil { return fmt.Errorf("failed to create collection_item_versions table: %w", err) } // Create indexes manually (sqlc doesn't generate CREATE INDEX functions for SQLite) 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);", "CREATE INDEX IF NOT EXISTS idx_collections_site_id ON collections(site_id);", "CREATE INDEX IF NOT EXISTS idx_collections_updated_at ON collections(updated_at);", "CREATE INDEX IF NOT EXISTS idx_collection_templates_lookup ON collection_templates(collection_id, site_id);", "CREATE INDEX IF NOT EXISTS idx_collection_templates_default ON collection_templates(collection_id, site_id, is_default DESC);", "CREATE INDEX IF NOT EXISTS idx_collection_items_lookup ON collection_items(collection_id, site_id, position);", "CREATE INDEX IF NOT EXISTS idx_collection_items_template ON collection_items(template_id);", "CREATE INDEX IF NOT EXISTS idx_collection_item_versions_lookup ON collection_item_versions(item_id, collection_id, site_id, created_at DESC);", "CREATE UNIQUE INDEX IF NOT EXISTS idx_collection_templates_one_default ON collection_templates(collection_id, site_id) WHERE is_default = 1;", } for _, query := range indexQueries { if _, err := db.conn.Exec(query); err != nil { return fmt.Errorf("failed to create index: %w", err) } } // Create update triggers manually (sqlc doesn't generate trigger creation functions) triggerQueries := []string{ `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;`, `CREATE TRIGGER IF NOT EXISTS update_collections_updated_at AFTER UPDATE ON collections FOR EACH ROW BEGIN UPDATE collections SET updated_at = strftime('%s', 'now') WHERE id = NEW.id AND site_id = NEW.site_id; END;`, `CREATE TRIGGER IF NOT EXISTS update_collection_items_updated_at AFTER UPDATE ON collection_items FOR EACH ROW BEGIN UPDATE collection_items SET updated_at = strftime('%s', 'now') WHERE item_id = NEW.item_id AND collection_id = NEW.collection_id AND site_id = NEW.site_id; END;`, } for _, query := range triggerQueries { if _, err := db.conn.Exec(query); err != nil { return fmt.Errorf("failed to create trigger: %w", err) } } return nil } // initializePostgreSQLSchema sets up the PostgreSQL database schema func (db *Database) initializePostgreSQLSchema() error { ctx := context.Background() // Create tables using sqlc-generated functions 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 collection tables if err := db.postgresqlQueries.InitializeCollectionsTable(ctx); err != nil { return fmt.Errorf("failed to create collections table: %w", err) } if err := db.postgresqlQueries.InitializeCollectionTemplatesTable(ctx); err != nil { return fmt.Errorf("failed to create collection_templates table: %w", err) } if err := db.postgresqlQueries.InitializeCollectionItemsTable(ctx); err != nil { return fmt.Errorf("failed to create collection_items table: %w", err) } if err := db.postgresqlQueries.InitializeCollectionItemVersionsTable(ctx); err != nil { return fmt.Errorf("failed to create collection_item_versions table: %w", err) } // Create indexes using sqlc-generated functions (PostgreSQL supports this) 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 collection indexes using sqlc-generated functions if err := db.postgresqlQueries.CreateCollectionsSiteIndex(ctx); err != nil { return fmt.Errorf("failed to create collections site index: %w", err) } if err := db.postgresqlQueries.CreateCollectionsUpdatedAtIndex(ctx); err != nil { return fmt.Errorf("failed to create collections updated_at index: %w", err) } if err := db.postgresqlQueries.CreateCollectionTemplatesLookupIndex(ctx); err != nil { return fmt.Errorf("failed to create collection templates lookup index: %w", err) } if err := db.postgresqlQueries.CreateCollectionTemplatesDefaultIndex(ctx); err != nil { return fmt.Errorf("failed to create collection templates default index: %w", err) } if err := db.postgresqlQueries.CreateCollectionItemsLookupIndex(ctx); err != nil { return fmt.Errorf("failed to create collection items lookup index: %w", err) } if err := db.postgresqlQueries.CreateCollectionItemsTemplateIndex(ctx); err != nil { return fmt.Errorf("failed to create collection items template index: %w", err) } if err := db.postgresqlQueries.CreateCollectionItemVersionsLookupIndex(ctx); err != nil { return fmt.Errorf("failed to create collection item versions lookup index: %w", err) } if err := db.postgresqlQueries.CreateCollectionTemplatesOneDefaultIndex(ctx); err != nil { return fmt.Errorf("failed to create collection templates one default constraint: %w", err) } // Create update function using sqlc-generated function if err := db.postgresqlQueries.CreateUpdateFunction(ctx); err != nil { return fmt.Errorf("failed to create update function: %w", err) } // Create triggers manually (sqlc doesn't generate trigger creation functions) triggerQueries := []string{ `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();`, `DROP TRIGGER IF EXISTS update_collections_updated_at ON collections; CREATE TRIGGER update_collections_updated_at BEFORE UPDATE ON collections FOR EACH ROW EXECUTE FUNCTION update_content_timestamp();`, `DROP TRIGGER IF EXISTS update_collection_items_updated_at ON collection_items; CREATE TRIGGER update_collection_items_updated_at BEFORE UPDATE ON collection_items FOR EACH ROW EXECUTE FUNCTION update_content_timestamp();`, } for _, query := range triggerQueries { if _, err := db.conn.Exec(query); err != nil { return fmt.Errorf("failed to create trigger: %w", err) } } return nil }