Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

at icy/yovxsu 3.9 kB View raw
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}