Files
insertr/internal/db/database.go
Joakim 824719f07d Implement collection item reordering with bulk operations and persistent HTML attributes
- Add bulk reorder API endpoint (PUT /api/collections/{id}/reorder) with atomic transactions
- Replace individual position updates with efficient bulk operations in frontend
- Implement unified ID generation and proper data-item-id injection during enhancement
- Fix collection item position persistence through content edit cycles
- Add optimistic UI with rollback capability for better user experience
- Update sqlc queries to include last_edited_by fields in position updates
- Remove obsolete data-content-type attributes and unify naming conventions
2025-10-07 22:59:00 +02:00

298 lines
10 KiB
Go

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
}