manage.go

69 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
package database

import (
	"fmt"
	"strings"
)

// This file owns Collection setup: deriving a table from a struct, auto-creating
// and migrating it, and declaring indexes. It is the boot-time, declarative half
// of a Collection; collection.go owns the per-request CRUD half. Schema failures
// here panic — they are programmer/configuration errors surfaced at startup.

// ManageOption configures a Collection during creation.
type ManageOption[E any] func(*Collection[E])

// WithIndex creates a non-unique index on the given columns.
func WithIndex[E any](columns ...string) ManageOption[E] {
	return func(c *Collection[E]) {
		indexName := fmt.Sprintf("idx_%s_%s", strings.ToLower(c.table), strings.ToLower(strings.Join(columns, "_")))
		query := fmt.Sprintf(`CREATE INDEX IF NOT EXISTS "%s" ON "%s"(%s)`, indexName, c.table, strings.Join(columns, ", "))
		if _, err := c.db.Exec(query); err != nil {
			panic(fmt.Sprintf("database: create index %s: %v", indexName, err))
		}
	}
}

// WithUniqueIndex creates a unique index on the given column(s). Pass multiple
// columns for a composite constraint, e.g. WithUniqueIndex("UserID", "TileID").
func WithUniqueIndex[E any](columns ...string) ManageOption[E] {
	return func(c *Collection[E]) {
		indexName := fmt.Sprintf("idx_%s_%s_unique", strings.ToLower(c.table), strings.ToLower(strings.Join(columns, "_")))
		query := fmt.Sprintf(`CREATE UNIQUE INDEX IF NOT EXISTS "%s" ON "%s"(%s)`, indexName, c.table, strings.Join(columns, ", "))
		if _, err := c.db.Exec(query); err != nil {
			panic(fmt.Sprintf("database: create unique index %s: %v", indexName, err))
		}
	}
}

// Manage returns a Collection for the entity type, deriving the table name from
// the struct name. The table is created if absent and migrated (missing columns
// added) if present, then the options run. Schema is declarative and runs at
// startup, so failures panic.
func Manage[E any](db *Database, model *E, opts ...ManageOption[E]) *Collection[E] {
	m := reflectType[E]()
	c := &Collection[E]{db: db, table: m.Name(), mirror: m}

	if !c.db.TableExists(c.table) {
		if err := c.db.CreateTable(c.table, c.mirror.Columns()); err != nil {
			panic(fmt.Sprintf("database: create table %s: %v", c.table, err))
		}
	} else {
		existing := make(map[string]bool)
		for _, name := range c.db.GetColumns(c.table) {
			existing[name] = true
		}
		for _, col := range c.mirror.Columns() {
			if !existing[col.Name] {
				if err := c.db.AddColumn(c.table, col); err != nil {
					panic(fmt.Sprintf("database: add column %s.%s: %v", c.table, col.Name, err))
				}
			}
		}
	}

	for _, opt := range opts {
		opt(c)
	}
	return c
}