an ORM-free SQL experience

add update stmt

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 4448ae32 3246a16e

verified
+2 -2
expr.go
··· 117 117 func Lt(left string, right any) Expr { return buildBinExpr(IdentExpr(left), "<", right) } 118 118 func Lte(left string, right any) Expr { return buildBinExpr(IdentExpr(left), "<=", right) } 119 119 120 - func (l Expr) And(r Expr) Expr { return BinExpr{l, "and", r}.AsExpr() } 121 - func (l Expr) Or(r Expr) Expr { return BinExpr{l, "or", r}.AsExpr() } 120 + func (l Expr) And(r Expr) Expr { return BinExpr{l, "AND", r}.AsExpr() } 121 + func (l Expr) Or(r Expr) Expr { return BinExpr{l, "OR", r}.AsExpr() }
+3 -3
expr_test.go
··· 74 74 right := Eq("status", "active") 75 75 76 76 andExpr := left.And(right) 77 - expectedAnd := "((age) = (?)) and ((status) = (?))" 77 + expectedAnd := "((age) = (?)) AND ((status) = (?))" 78 78 if andExpr.String() != expectedAnd { 79 79 t.Errorf("Expected '%s', got '%s'", expectedAnd, andExpr.String()) 80 80 } 81 81 82 82 orExpr := left.Or(right) 83 - expectedOr := "((age) = (?)) or ((status) = (?))" 83 + expectedOr := "((age) = (?)) OR ((status) = (?))" 84 84 if orExpr.String() != expectedOr { 85 85 t.Errorf("Expected '%s', got '%s'", expectedOr, orExpr.String()) 86 86 } ··· 94 94 complex := age. 95 95 And(status). 96 96 Or(score) 97 - expected := "(((age) = (?)) and ((status) = (?))) or ((score) > (?))" 97 + expected := "(((age) = (?)) AND ((status) = (?))) OR ((score) > (?))" 98 98 99 99 if complex.String() != expected { 100 100 t.Errorf("Expected '%s', got '%s'", expected, complex.String())
+2 -2
select_test.go
··· 67 67 GroupBy("department"). 68 68 OrderBy("name", Ascending). 69 69 Limit(10), 70 - expectedSql: "SELECT name, age, department FROM users WHERE ((active) = (?)) and ((age) > (?)) GROUP BY department ORDER BY name asc LIMIT 10", 70 + expectedSql: "SELECT name, age, department FROM users WHERE ((active) = (?)) AND ((age) > (?)) GROUP BY department ORDER BY name asc LIMIT 10", 71 71 expectedArgs: []any{true, 18}, 72 72 }, 73 73 } ··· 184 184 expectedRows: 2, 185 185 }, 186 186 { 187 - name: "Select users with salary between 70000 and 80000", 187 + name: "Select users with salary between 70000 AND 80000", 188 188 stmt: Select("name", "salary"). 189 189 From("users"). 190 190 Where(Gte("salary", 70000.0).And(Lte("salary", 80000.0))),
+143
update.go
··· 1 + package norm 2 + 3 + import ( 4 + "context" 5 + "database/sql" 6 + "fmt" 7 + "strings" 8 + ) 9 + 10 + type update struct { 11 + table string 12 + or UpdateOr 13 + sets []struct { 14 + col string 15 + val any 16 + } 17 + where *Expr 18 + } 19 + 20 + type UpdateOr int 21 + 22 + const ( 23 + UpdateNone UpdateOr = iota 24 + UpdateAbort 25 + UpdateFail 26 + UpdateIgnore 27 + UpdateReplace 28 + UpdateRollback 29 + ) 30 + 31 + func (u UpdateOr) String() string { 32 + switch u { 33 + case UpdateAbort: 34 + return "ABORT" 35 + case UpdateFail: 36 + return "FAIL" 37 + case UpdateIgnore: 38 + return "IGNORE" 39 + case UpdateReplace: 40 + return "REPLACE" 41 + case UpdateRollback: 42 + return "ROLLBACK" 43 + default: 44 + return "" 45 + } 46 + } 47 + 48 + func Update(table string) update { 49 + return update{table: table} 50 + } 51 + 52 + type UpdateOpt func(s *update) 53 + 54 + func (u update) Or(option UpdateOr) update { 55 + u.or = option 56 + return u 57 + } 58 + 59 + func (u update) Set(column string, value any) update { 60 + u.sets = append(u.sets, struct { 61 + col string 62 + val any 63 + }{ 64 + column, value, 65 + }) 66 + return u 67 + } 68 + 69 + func (u update) Sets(values map[string]any) update { 70 + for column, value := range values { 71 + u = u.Set(column, value) 72 + } 73 + return u 74 + } 75 + 76 + func (u update) Where(expr Expr) update { 77 + u.where = &expr 78 + return u 79 + } 80 + 81 + func (u update) Compile() (string, []any, error) { 82 + var sql strings.Builder 83 + var args []any 84 + 85 + sql.WriteString("UPDATE ") 86 + 87 + orKw := u.or.String() 88 + if orKw != "" { 89 + sql.WriteString("OR ") 90 + sql.WriteString(u.or.String()) 91 + sql.WriteString(" ") 92 + } 93 + 94 + if u.table == "" { 95 + return "", nil, fmt.Errorf("table name is required") 96 + } 97 + sql.WriteString(u.table) 98 + 99 + if len(u.sets) == 0 { 100 + return "", nil, fmt.Errorf("no SET clauses supplied") 101 + } 102 + 103 + sql.WriteString(" SET ") 104 + 105 + for i, set := range u.sets { 106 + if i != 0 { 107 + sql.WriteString(", ") 108 + } 109 + sql.WriteString(set.col) 110 + sql.WriteString(" = ?") 111 + args = append(args, set.val) 112 + } 113 + 114 + if u.where != nil { 115 + sql.WriteString(" WHERE ") 116 + sql.WriteString(u.where.String()) 117 + 118 + args = append(args, u.where.Binds()...) 119 + } 120 + 121 + return sql.String(), args, nil 122 + } 123 + 124 + func (u update) MustCompile() (string, []any) { 125 + sql, args, err := u.Compile() 126 + if err != nil { 127 + panic(err) 128 + } 129 + 130 + return sql, args 131 + } 132 + 133 + func (u update) Build(p Database) (*sql.Stmt, []any, error) { return Build(u, p) } 134 + func (u update) MustBuild(p Database) (*sql.Stmt, []any) { return MustBuild(u, p) } 135 + 136 + func (u update) Exec(p Database) (sql.Result, error) { return Exec(u, p) } 137 + func (u update) ExecContext(ctx context.Context, p Database) (sql.Result, error) { 138 + return ExecContext(ctx, u, p) 139 + } 140 + func (u update) MustExec(p Database) sql.Result { return MustExec(u, p) } 141 + func (u update) MustExecContext(ctx context.Context, p Database) sql.Result { 142 + return MustExecContext(ctx, u, p) 143 + }
+301
update_test.go
··· 1 + package norm 2 + 3 + import ( 4 + "slices" 5 + "testing" 6 + 7 + _ "github.com/mattn/go-sqlite3" 8 + ) 9 + 10 + func TestUpdateBuild_Success(t *testing.T) { 11 + tests := []struct { 12 + name string 13 + stmt Compiler 14 + expectedSql string 15 + expectedArgs []any 16 + }{ 17 + { 18 + name: "Simple update", 19 + stmt: Update("users").Set("name", "John"), 20 + expectedSql: "UPDATE users SET name = ?", 21 + expectedArgs: []any{"John"}, 22 + }, 23 + { 24 + name: "Update with WHERE", 25 + stmt: Update("users").Set("name", "John").Where(Eq("id", 1)), 26 + expectedSql: "UPDATE users SET name = ? WHERE (id) = (?)", 27 + expectedArgs: []any{"John", 1}, 28 + }, 29 + { 30 + name: "Abort clause", 31 + stmt: Update("users").Or(UpdateAbort).Set("name", "John"), 32 + expectedSql: "UPDATE OR ABORT users SET name = ?", 33 + expectedArgs: []any{"John"}, 34 + }, 35 + { 36 + name: "Ignore clause", 37 + stmt: Update("users").Or(UpdateIgnore).Set("name", "John"), 38 + expectedSql: "UPDATE OR IGNORE users SET name = ?", 39 + expectedArgs: []any{"John"}, 40 + }, 41 + { 42 + name: "Fail clause", 43 + stmt: Update("users").Or(UpdateFail).Set("name", "John"), 44 + expectedSql: "UPDATE OR FAIL users SET name = ?", 45 + expectedArgs: []any{"John"}, 46 + }, 47 + { 48 + name: "Replace clause", 49 + stmt: Update("users").Or(UpdateReplace).Set("name", "John"), 50 + expectedSql: "UPDATE OR REPLACE users SET name = ?", 51 + expectedArgs: []any{"John"}, 52 + }, 53 + { 54 + name: "Rollback clause", 55 + stmt: Update("users").Or(UpdateRollback).Set("name", "John"), 56 + expectedSql: "UPDATE OR ROLLBACK users SET name = ?", 57 + expectedArgs: []any{"John"}, 58 + }, 59 + { 60 + name: "Default clause", 61 + stmt: Update("users").Or(UpdateOr(10)).Set("name", "John"), 62 + expectedSql: "UPDATE users SET name = ?", 63 + expectedArgs: []any{"John"}, 64 + }, 65 + { 66 + name: "Multiple sets", 67 + stmt: Update("users").Set("name", "John").Set("age", 35), 68 + expectedSql: "UPDATE users SET name = ?, age = ?", 69 + expectedArgs: []any{"John", 35}, 70 + }, 71 + { 72 + name: "Multiple WHERE conditions", 73 + stmt: Update("users").Set("salary", 90000.0).Where(Eq("department", "Engineering").And(Eq("active", true))), 74 + expectedSql: "UPDATE users SET salary = ? WHERE ((department) = (?)) AND ((active) = (?))", 75 + expectedArgs: []any{90000.0, "Engineering", true}, 76 + }, 77 + { 78 + name: "Complex update", 79 + stmt: Update("users").Or(UpdateIgnore).Set("name", "Updated").Set("salary", 100000.0).Where(Gt("age", 30).And(Eq("active", true))), 80 + expectedSql: "UPDATE OR IGNORE users SET name = ?, salary = ? WHERE ((age) > (?)) AND ((active) = (?))", 81 + expectedArgs: []any{"Updated", 100000.0, 30, true}, 82 + }, 83 + } 84 + 85 + for _, test := range tests { 86 + t.Run(test.name, func(t *testing.T) { 87 + sql, args := test.stmt.MustCompile() 88 + 89 + if sql != test.expectedSql { 90 + t.Errorf("Expected '%s', got '%s'", test.expectedSql, sql) 91 + } 92 + 93 + if len(args) != len(test.expectedArgs) { 94 + t.Errorf("Expected '%d' args, got '%d' args", len(test.expectedArgs), len(args)) 95 + } 96 + 97 + for i := range len(args) { 98 + if args[i] != test.expectedArgs[i] { 99 + t.Errorf("Expected '%v', got '%v' at index %d", test.expectedArgs[i], args[i], i) 100 + } 101 + } 102 + }) 103 + } 104 + } 105 + 106 + func TestUpdateSetMap_Build(t *testing.T) { 107 + tests := []struct { 108 + name string 109 + stmt Compiler 110 + expectedConfig []struct { 111 + sql string 112 + args []any 113 + } 114 + }{ 115 + { 116 + name: "Sets with map", 117 + stmt: Update("users").Sets(map[string]any{ 118 + "name": "John", 119 + "age": 25, 120 + }).Where(Eq("id", 1)), 121 + expectedConfig: []struct { 122 + sql string 123 + args []any 124 + }{ 125 + { 126 + sql: "UPDATE users SET name = ?, age = ? WHERE (id) = (?)", 127 + args: []any{"John", 25, 1}, 128 + }, 129 + { 130 + sql: "UPDATE users SET age = ?, name = ? WHERE (id) = (?)", 131 + args: []any{25, "John", 1}, 132 + }, 133 + }, 134 + }, 135 + { 136 + name: "Mixed individual and map sets", 137 + stmt: Update("users").Set("active", false).Sets(map[string]any{ 138 + "name": "Updated User", 139 + "salary": 95000.0, 140 + }), 141 + expectedConfig: []struct { 142 + sql string 143 + args []any 144 + }{ 145 + { 146 + sql: "UPDATE users SET active = ?, name = ?, salary = ?", 147 + args: []any{false, "Updated User", 95000.0}, 148 + }, 149 + { 150 + sql: "UPDATE users SET active = ?, salary = ?, name = ?", 151 + args: []any{false, 95000.0, "Updated User"}, 152 + }, 153 + }, 154 + }, 155 + } 156 + 157 + for _, test := range tests { 158 + t.Run(test.name, func(t *testing.T) { 159 + sql, args := test.stmt.MustCompile() 160 + 161 + any := false 162 + idx := 0 163 + for i, config := range test.expectedConfig { 164 + idx = i 165 + equalSql := config.sql == sql 166 + equalArgs := slices.Equal(config.args, args) 167 + if equalSql && equalArgs { 168 + any = true 169 + } 170 + } 171 + 172 + if !any { 173 + t.Errorf("Config did not match: %d: %q; got %q, %q", idx, test.expectedConfig[idx], sql, args) 174 + } 175 + 176 + }) 177 + } 178 + } 179 + 180 + func TestUpdateCompileFail(t *testing.T) { 181 + tests := []struct { 182 + name string 183 + stmt Compiler 184 + expectedError string 185 + }{ 186 + { 187 + name: "No SET clauses", 188 + stmt: Update("users"), 189 + expectedError: "no SET clauses supplied", 190 + }, 191 + } 192 + 193 + for _, test := range tests { 194 + t.Run(test.name, func(t *testing.T) { 195 + sql, args, err := test.stmt.Compile() 196 + if err == nil { 197 + t.Error("Expected error, got nil") 198 + } 199 + 200 + if err.Error() != test.expectedError { 201 + t.Errorf("Expected error '%s', got '%s'", test.expectedError, err.Error()) 202 + } 203 + 204 + if sql != "" { 205 + t.Errorf("Expected empty SQL on error, got '%s'", sql) 206 + } 207 + 208 + if args != nil { 209 + t.Errorf("Expected empty args on error, got '%q'", args) 210 + } 211 + }) 212 + } 213 + } 214 + 215 + func TestUpdateIntegration(t *testing.T) { 216 + tests := []struct { 217 + name string 218 + stmt Execer 219 + expectedRows int64 220 + }{ 221 + { 222 + name: "Update all users salary", 223 + stmt: Update("users").Set("salary", 100000.0), 224 + expectedRows: 6, 225 + }, 226 + { 227 + name: "Update active users only", 228 + stmt: Update("users"). 229 + Set("department", "Updated Department"). 230 + Where(Eq("active", true)), 231 + expectedRows: 4, 232 + }, 233 + { 234 + name: "Update users in Engineering", 235 + stmt: Update("users"). 236 + Set("salary", 95000.0). 237 + Where(Eq("department", "Engineering")), 238 + expectedRows: 3, 239 + }, 240 + { 241 + name: "Update users with age > 30", 242 + stmt: Update("users"). 243 + Set("active", false). 244 + Where(Gt("age", 30)), 245 + expectedRows: 2, 246 + }, 247 + { 248 + name: "Update users with multiple conditions", 249 + stmt: Update("users"). 250 + Set("salary", 120000.0). 251 + Where(Eq("department", "Engineering").And(Eq("active", true))), 252 + expectedRows: 2, 253 + }, 254 + { 255 + name: "Update with OR IGNORE (no conflict expected)", 256 + stmt: Update("users"). 257 + Or(UpdateIgnore). 258 + Set("name", "Updated Name"). 259 + Where(Eq("id", 1)), 260 + expectedRows: 1, 261 + }, 262 + { 263 + name: "Update multiple fields", 264 + stmt: Update("users"). 265 + Sets(map[string]any{ 266 + "active": true, 267 + "salary": 110000.0, 268 + }). 269 + Where(Eq("department", "Marketing")), 270 + expectedRows: 2, 271 + }, 272 + { 273 + name: "Update with no matching rows", 274 + stmt: Update("users"). 275 + Set("name", "Non-existent"). 276 + Where(Eq("id", 999)), 277 + expectedRows: 0, 278 + }, 279 + } 280 + 281 + for _, test := range tests { 282 + t.Run(test.name, func(t *testing.T) { 283 + db := setupTestDB(t) 284 + defer db.Close() 285 + 286 + res, err := test.stmt.Exec(db) 287 + if err != nil { 288 + t.Fatalf("Failed to execute query: %v", err) 289 + } 290 + 291 + count, err := res.RowsAffected() 292 + if err != nil { 293 + t.Fatalf("Failed to get rows affected: %v", err) 294 + } 295 + 296 + if count != test.expectedRows { 297 + t.Errorf("Expected %d rows, got %d", test.expectedRows, count) 298 + } 299 + }) 300 + } 301 + }