peachy-go/db.go

260 lines
5.7 KiB
Go
Raw Normal View History

2024-08-16 06:27:35 +00:00
package peachy
import (
2024-11-17 01:21:51 +00:00
_ "embed"
2024-08-16 06:27:35 +00:00
"errors"
"fmt"
2024-11-01 22:32:02 +00:00
"log"
2024-08-16 06:27:35 +00:00
"os"
2024-10-31 19:18:25 +00:00
"strings"
2024-08-16 06:27:35 +00:00
2024-10-31 19:18:25 +00:00
"git.codemonkeysoftware.net/b/peachy-go/csexp"
"git.codemonkeysoftware.net/b/peachy-go/csexp/match"
2024-08-16 06:27:35 +00:00
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
const AppID = '🍑'
var ErrInvalidDB = errors.New("invalid database file")
var ErrFileExists = errors.New("database file already exists")
var ErrFileNotExist = errors.New("database file does not exist")
type DBError struct{ error }
func (dbe DBError) Error() string {
return "database error: " + dbe.error.Error()
}
type DB struct {
conn *sqlite.Conn
}
2024-11-17 01:21:51 +00:00
func (db DB) Close() {
if db.conn != nil {
db.conn.Close()
}
2024-08-16 06:27:35 +00:00
}
2024-11-01 22:32:02 +00:00
var tableNameMatcher = match.MustCompile("(%s%s)")
2024-10-31 19:18:25 +00:00
2024-11-01 22:32:02 +00:00
func parseTableName(ctx sqlite.Context, args []sqlite.Value) (sqlite.Value, error) {
2024-10-31 19:18:25 +00:00
rawName := args[0].Text()
2024-11-01 22:32:02 +00:00
classOrName := args[1].Text()
var class, name string
var result *string
switch classOrName {
case "class":
result = &class
case "name":
result = &name
default:
return sqlite.Value{}, errors.New(`parse_table_name: 2nd arg must be "class" or "name"`)
}
2024-10-31 19:18:25 +00:00
sexp, err := csexp.ParseString(rawName)
if err != nil {
return sqlite.Value{}, nil
}
2024-11-01 22:32:02 +00:00
err = tableNameMatcher.Match(sexp, &class, &name)
2024-10-31 19:18:25 +00:00
if err != nil {
2024-11-01 22:32:02 +00:00
log.Printf("parseTableName: unexpected sexp structure for %s", rawName)
2024-10-31 19:18:25 +00:00
return sqlite.Value{}, nil
}
2024-11-01 22:32:02 +00:00
return sqlite.TextValue(*result), nil
2024-10-31 19:18:25 +00:00
}
2024-08-16 06:27:35 +00:00
func setupConn(conn *sqlite.Conn) error {
2024-11-01 22:32:02 +00:00
return conn.CreateFunction("parse_table_name", &sqlite.FunctionImpl{
NArgs: 2,
2024-10-31 19:18:25 +00:00
Deterministic: true,
AllowIndirect: true,
2024-11-01 22:32:02 +00:00
Scalar: parseTableName,
2024-10-31 19:18:25 +00:00
})
2024-08-16 06:27:35 +00:00
}
2024-11-17 01:21:51 +00:00
func Open(path string) (db DB, err error) {
2024-08-16 06:27:35 +00:00
var conn *sqlite.Conn
defer func() {
if err != nil {
conn.Close()
}
}()
conn, err = sqlite.OpenConn(path, sqlite.OpenReadWrite|sqlite.OpenWAL)
switch sqlite.ErrCode(err) {
case sqlite.ResultOK:
case sqlite.ResultCantOpen:
2024-11-17 01:21:51 +00:00
return db, ErrFileNotExist
2024-08-16 06:27:35 +00:00
case sqlite.ResultNotADB:
2024-11-17 01:21:51 +00:00
return db, ErrInvalidDB
2024-08-16 06:27:35 +00:00
default:
2024-11-17 01:21:51 +00:00
return db, DBError{err}
2024-08-16 06:27:35 +00:00
}
var goodAppID bool
sqlitex.ExecuteTransient(conn, "PRAGMA application_id", &sqlitex.ExecOptions{
ResultFunc: func(stmt *sqlite.Stmt) error {
goodAppID = stmt.ColumnInt32(0) == AppID
return nil
}})
if !goodAppID {
2024-11-17 01:21:51 +00:00
return db, ErrInvalidDB
2024-08-16 06:27:35 +00:00
}
err = setupConn(conn)
if err != nil {
2024-11-17 01:21:51 +00:00
return db, err
2024-08-16 06:27:35 +00:00
}
2024-11-17 01:21:51 +00:00
return DB{conn: conn}, nil
2024-08-16 06:27:35 +00:00
}
2024-11-17 01:21:51 +00:00
//go:embed metadata_schema.sql
var metadataSchema string
func Create(path string) (db DB, err error) {
fmt.Println("metadata schema:\n", metadataSchema)
2024-08-16 06:27:35 +00:00
var conn *sqlite.Conn
defer func() {
if err != nil {
conn.Close()
}
}()
finfo, _ := os.Stat(path)
if finfo != nil {
2024-11-17 01:21:51 +00:00
return db, ErrFileExists
2024-08-16 06:27:35 +00:00
}
conn, err = sqlite.OpenConn(path, sqlite.OpenCreate|sqlite.OpenReadWrite|sqlite.OpenWAL)
if err != nil {
2024-11-17 01:21:51 +00:00
return db, fmt.Errorf("could not create database: %w", err)
2024-08-16 06:27:35 +00:00
}
query := fmt.Sprintf("PRAGMA application_id=%d", AppID)
err = sqlitex.ExecuteTransient(conn, query, nil)
if err != nil {
2024-11-17 01:21:51 +00:00
return db, DBError{err}
}
err = sqlitex.ExecuteScript(conn, metadataSchema, nil)
if err != nil {
return db, DBError{err}
2024-08-16 06:27:35 +00:00
}
2024-11-17 01:21:51 +00:00
2024-08-16 06:27:35 +00:00
err = setupConn(conn)
if err != nil {
2024-11-17 01:21:51 +00:00
return db, err
2024-08-16 06:27:35 +00:00
}
2024-11-17 01:21:51 +00:00
return DB{conn: conn}, nil
2024-08-16 06:27:35 +00:00
}
2024-10-31 19:18:25 +00:00
func quoteName(name string) string {
return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
}
2024-11-01 22:32:02 +00:00
type CompositeKind int
2024-10-31 19:18:25 +00:00
2024-11-01 22:32:02 +00:00
const (
Record CompositeKind = iota
Variant
)
type CompositeType struct {
2024-10-31 19:18:25 +00:00
Name string
2024-11-01 22:32:02 +00:00
Kind CompositeKind
2024-10-31 19:18:25 +00:00
}
2024-11-01 22:32:02 +00:00
const addConcreteCompositeTypeQuery = `CREATE TABLE %s (
id INTEGER PRIMARY KEY
);`
const addAbstractCompositeTypeQuery = `CREATE TABLE %s (
id INTEGER PRIMARY KEY,
%s_value_id INTEGER NOT NULL REFERENCES %s(id)
);`
2024-11-17 01:21:51 +00:00
func (db DB) AddCompositeType(name string, kind CompositeKind) error {
2024-11-01 22:32:02 +00:00
var kindStr string
switch kind {
case Record:
kindStr = "record"
case Variant:
kindStr = "variant"
default:
return errors.New("invalid kind")
}
abstractTableName := quoteName(csexp.List{
csexp.Atom("composite-value"),
csexp.Atom(name),
}.String())
concreteTableName := quoteName(csexp.List{
csexp.Atom(kindStr + "-value"),
csexp.Atom(name),
}.String())
err := sqlitex.Execute(db.conn, fmt.Sprintf(addConcreteCompositeTypeQuery, concreteTableName), nil)
2024-10-31 19:18:25 +00:00
if err != nil {
2024-11-01 22:32:02 +00:00
return fmt.Errorf("AddCompositeType: %w", err)
2024-10-31 19:18:25 +00:00
}
2024-11-01 22:32:02 +00:00
err = sqlitex.Execute(db.conn, fmt.Sprintf(addAbstractCompositeTypeQuery, abstractTableName, kindStr, concreteTableName), nil)
if err != nil {
return fmt.Errorf("AddCompositeType: %w", err)
}
2024-10-31 19:18:25 +00:00
return nil
}
2024-11-01 22:32:02 +00:00
const getCompositeTypesQuery = `SELECT
parse_table_name(name, 'name'),
parse_table_name(name, 'class')
2024-10-31 19:18:25 +00:00
FROM sqlite_schema
2024-11-01 22:32:02 +00:00
WHERE parse_table_name(name, 'class') IN ('record-value', 'variant-value')
ORDER BY parse_table_name(name, 'name') ASC;`
2024-10-31 19:18:25 +00:00
2024-11-17 01:21:51 +00:00
func (db DB) GetCompositeTypes() ([]CompositeType, error) {
2024-11-01 22:32:02 +00:00
var results []CompositeType
err := sqlitex.Execute(db.conn, getCompositeTypesQuery, &sqlitex.ExecOptions{
2024-10-31 19:18:25 +00:00
ResultFunc: func(stmt *sqlite.Stmt) error {
2024-11-01 22:32:02 +00:00
result := CompositeType{Name: stmt.ColumnText(0)}
if stmt.ColumnText(1) == "record-value" {
result.Kind = Record
} else {
result.Kind = Variant
}
results = append(results, result)
2024-10-31 19:18:25 +00:00
return nil
},
})
if err != nil {
2024-11-01 22:32:02 +00:00
return nil, fmt.Errorf("GetCompositeTypes: %w", err)
2024-10-31 19:18:25 +00:00
}
return results, nil
}
2024-11-17 01:21:51 +00:00
func (db DB) SiteName() (string, error) {
var name string
err := sqlitex.Execute(db.conn, "SELECT name FROM site_metadata", &sqlitex.ExecOptions{
ResultFunc: func(stmt *sqlite.Stmt) error {
name = stmt.ColumnText(0)
return nil
},
})
if err != nil {
err = fmt.Errorf("SiteName: %w", err)
}
return name, err
}
func (db DB) SetSiteName(name string) error {
err := sqlitex.Execute(db.conn, "UPDATE site_metadata SET name = ?", &sqlitex.ExecOptions{
Args: []any{name},
})
if err != nil {
return fmt.Errorf("SetSiteName: %w", err)
}
return nil
}