Monorepo for Tangled
tangled.org
1package orm
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "log/slog"
9 "reflect"
10 "strings"
11
12 "github.com/mattn/go-sqlite3"
13)
14
15func IsUniqueViolation(err error) bool {
16 var sqlErr sqlite3.Error
17 if !errors.As(err, &sqlErr) {
18 return false
19 }
20 return sqlErr.ExtendedCode == sqlite3.ErrConstraintUnique ||
21 sqlErr.ExtendedCode == sqlite3.ErrConstraintPrimaryKey
22}
23
24type migrationFn = func(*sql.Tx) error
25
26func RunMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error {
27 logger = logger.With("migration", name)
28
29 tx, err := c.BeginTx(context.Background(), nil)
30 if err != nil {
31 return err
32 }
33 defer tx.Rollback()
34
35 var exists bool
36 err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists)
37 if err != nil {
38 return err
39 }
40
41 if !exists {
42 // run migration
43 err = migrationFn(tx)
44 if err != nil {
45 logger.Error("failed to run migration", "err", err)
46 return err
47 }
48
49 // mark migration as complete
50 _, err = tx.Exec("insert into migrations (name) values (?)", name)
51 if err != nil {
52 logger.Error("failed to mark migration as complete", "err", err)
53 return err
54 }
55
56 // commit the transaction
57 if err := tx.Commit(); err != nil {
58 return err
59 }
60
61 logger.Info("migration applied successfully")
62 } else {
63 logger.Warn("skipped migration, already applied")
64 }
65
66 return nil
67}
68
69type Filter struct {
70 Key string
71 arg any
72 Cmp string
73}
74
75func newFilter(key, cmp string, arg any) Filter {
76 return Filter{
77 Key: key,
78 arg: arg,
79 Cmp: cmp,
80 }
81}
82
83func FilterEq(key string, arg any) Filter { return newFilter(key, "=", arg) }
84func FilterNotEq(key string, arg any) Filter { return newFilter(key, "<>", arg) }
85func FilterGte(key string, arg any) Filter { return newFilter(key, ">=", arg) }
86func FilterLte(key string, arg any) Filter { return newFilter(key, "<=", arg) }
87func FilterIs(key string, arg any) Filter { return newFilter(key, "is", arg) }
88func FilterIsNot(key string, arg any) Filter { return newFilter(key, "is not", arg) }
89func FilterIn(key string, arg any) Filter { return newFilter(key, "in", arg) }
90func FilterLike(key string, arg any) Filter { return newFilter(key, "like", arg) }
91func FilterNotLike(key string, arg any) Filter { return newFilter(key, "not like", arg) }
92func FilterContains(key string, arg any) Filter {
93 return newFilter(key, "like", fmt.Sprintf("%%%v%%", arg))
94}
95
96// FilterInSubquery compiles to `key in (subquery)`, binding args within the
97// subquery. Prefer this over FilterIn with a large materialized list: it
98// keeps the query text constant and lets sqlite plan a semi-join.
99func FilterInSubquery(key, subquery string, args ...any) Filter {
100 return newFilter(key, "in", subqueryArg{query: subquery, args: args})
101}
102
103type subqueryArg struct {
104 query string
105 args []any
106}
107
108func (f Filter) Condition() string {
109 if sub, ok := f.arg.(subqueryArg); ok {
110 return fmt.Sprintf("%s %s (%s)", f.Key, f.Cmp, sub.query)
111 }
112
113 rv := reflect.ValueOf(f.arg)
114 kind := rv.Kind()
115
116 // if we have `FilterIn(k, [1, 2, 3])`, compile it down to `k in (?, ?, ?)`
117 if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
118 if rv.Len() == 0 {
119 // always false
120 return "1 = 0"
121 }
122
123 placeholders := make([]string, rv.Len())
124 for i := range placeholders {
125 placeholders[i] = "?"
126 }
127
128 return fmt.Sprintf("%s %s (%s)", f.Key, f.Cmp, strings.Join(placeholders, ", "))
129 }
130
131 return fmt.Sprintf("%s %s ?", f.Key, f.Cmp)
132}
133
134func (f Filter) Arg() []any {
135 if sub, ok := f.arg.(subqueryArg); ok {
136 return sub.args
137 }
138
139 rv := reflect.ValueOf(f.arg)
140 kind := rv.Kind()
141 if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
142 if rv.Len() == 0 {
143 return nil
144 }
145
146 out := make([]any, rv.Len())
147 for i := range rv.Len() {
148 out[i] = rv.Index(i).Interface()
149 }
150 return out
151 }
152
153 return []any{f.arg}
154}