From 7164eeaed41b06fdb722e16c7eddc90343a08708 Mon Sep 17 00:00:00 2001 From: Andrey Kolkov Date: Fri, 3 Jul 2026 18:51:07 +0300 Subject: [PATCH] style: add spaces around SQL operators, fix package godoc, deprecation markers, safety docs Quality polish (P4): - CompareExp/HashExp/InExp: add spaces around operators for readable SQL output ("col" = ? instead of "col"=?) - Package godoc: updated to recommended API (db.Select, relica.Eq, db.Model) instead of deprecated db.Builder().Select("*") - NewDB: deprecated marker in standard gopls format - CaseWhen: WARNING in godoc about raw SQL condition strings - scanner: explicit nil pointer check with clear error message --- db.go | 24 ++++--- internal/core/aggregate_test.go | 2 +- internal/core/andwhere_orwhere_test.go | 20 +++--- internal/core/count_exists_tosql_test.go | 28 ++++---- internal/core/exists_test.go | 10 +-- internal/core/expression.go | 10 +-- internal/core/expression_test.go | 82 ++++++++++++------------ internal/core/func_expressions.go | 3 + internal/core/scanner.go | 3 + internal/core/subquery_test.go | 2 +- 10 files changed, 94 insertions(+), 90 deletions(-) diff --git a/db.go b/db.go index 09b2323..06aa979 100644 --- a/db.go +++ b/db.go @@ -22,29 +22,27 @@ // defer db.Close() // // var users []User -// err = db.Builder().Select("*").From("users").All(&users) +// err = db.Select("id", "name").From("users").All(&users) // // # Features // // CRUD Operations: // -// // SELECT -// db.Builder().Select("*").From("users").Where("id = ?", 123).One(&user) +// // SELECT with Expression API +// db.Select().From("users").Where(relica.Eq("id", 123)).One(&user) // -// // INSERT -// db.Builder().Insert("users", map[string]interface{}{ -// "name": "Alice", -// "email": "alice@example.com", -// }).Execute() +// // INSERT via Model API (recommended) +// user := User{Name: "Alice", Email: "alice@example.com"} +// db.Model(&user).Insert() // // // UPDATE -// db.Builder().Update("users"). +// db.Update("users"). // Set(map[string]interface{}{"status": "active"}). -// Where("id = ?", 123). +// Where(relica.Eq("id", 123)). // Execute() // // // DELETE -// db.Builder().Delete("users").Where("id = ?", 123).Execute() +// db.Delete("users").Where(relica.Eq("id", 123)).Execute() package relica import ( @@ -302,9 +300,9 @@ func Open(driverName, dsn string, opts ...Option) (*DB, error) { return &DB{db: coreDB}, nil } -// NewDB creates a database connection (deprecated: use Open). +// NewDB creates a database connection. // -// This function exists for backward compatibility. New code should use Open. +// Deprecated: Use Open instead. NewDB will be removed in a future version. // // Example: // diff --git a/internal/core/aggregate_test.go b/internal/core/aggregate_test.go index 898372b..d6a1724 100644 --- a/internal/core/aggregate_test.go +++ b/internal/core/aggregate_test.go @@ -210,7 +210,7 @@ func TestSelectQuery_Having_Expression(t *testing.T) { require.NotNil(t, q) // Verify HAVING clause with column expression - assert.Contains(t, q.sql, `HAVING "user_id">$1`) + assert.Contains(t, q.sql, `HAVING "user_id" > $1`) assert.Equal(t, []interface{}{100}, q.params) } diff --git a/internal/core/andwhere_orwhere_test.go b/internal/core/andwhere_orwhere_test.go index 121ae6c..6b165ad 100644 --- a/internal/core/andwhere_orwhere_test.go +++ b/internal/core/andwhere_orwhere_test.go @@ -56,7 +56,7 @@ func TestSelectQuery_AndWhere(t *testing.T) { Where("status = ?", 1). AndWhere(GreaterThan("age", 18)) }, - expectedSQL: `SELECT * FROM "users" WHERE status = $1 AND "age">$2`, + expectedSQL: `SELECT * FROM "users" WHERE status = $1 AND "age" > $2`, expectedLen: 2, }, { @@ -146,7 +146,7 @@ func TestSelectQuery_OrWhere(t *testing.T) { Where("status = ?", 1). OrWhere(Eq("role", "admin")) }, - expectedSQL: `SELECT * FROM "users" WHERE (status = $1) OR ("role"=$2)`, + expectedSQL: `SELECT * FROM "users" WHERE (status = $1) OR ("role" = $2)`, expectedLen: 2, }, { @@ -247,7 +247,7 @@ func TestUpdateQuery_AndWhere(t *testing.T) { Where("id > ?", 100). AndWhere(Eq("active", true)) }, - expectedSQL: `UPDATE "users" SET "status" = $1 WHERE id > $2 AND "active"=$3`, + expectedSQL: `UPDATE "users" SET "status" = $1 WHERE id > $2 AND "active" = $3`, expectedLen: 3, }, } @@ -307,7 +307,7 @@ func TestUpdateQuery_OrWhere(t *testing.T) { Where("banned = ?", true). OrWhere(Eq("deleted", true)) }, - expectedSQL: `UPDATE "users" SET "status" = $1 WHERE (banned = $2) OR ("deleted"=$3)`, + expectedSQL: `UPDATE "users" SET "status" = $1 WHERE (banned = $2) OR ("deleted" = $3)`, expectedLen: 3, }, } @@ -363,7 +363,7 @@ func TestDeleteQuery_AndWhere(t *testing.T) { Where("status = ?", 0). AndWhere(LessThan("created_at", "2020-01-01")) }, - expectedSQL: `DELETE FROM "users" WHERE status = $1 AND "created_at"<$2`, + expectedSQL: `DELETE FROM "users" WHERE status = $1 AND "created_at" < $2`, expectedLen: 2, }, } @@ -419,7 +419,7 @@ func TestDeleteQuery_OrWhere(t *testing.T) { Where("banned = ?", true). OrWhere(Eq("deleted", true)) }, - expectedSQL: `DELETE FROM "users" WHERE (banned = $1) OR ("deleted"=$2)`, + expectedSQL: `DELETE FROM "users" WHERE (banned = $1) OR ("deleted" = $2)`, expectedLen: 2, }, } @@ -549,9 +549,9 @@ func TestAndWhere_OrWhere_HashExp(t *testing.T) { // After sorting: WHERE status=1 AND (age=18 AND city=NYC) OR (role=admin) // With parentheses: (status=1 AND age=18 AND city=NYC) OR (role=admin) assert.Contains(t, q.sql, `WHERE (`) - assert.Contains(t, q.sql, `"status"=$1`) - assert.Contains(t, q.sql, `"age"=$2`) - assert.Contains(t, q.sql, `"city"=$3`) - assert.Contains(t, q.sql, `OR ("role"=$4)`) + assert.Contains(t, q.sql, `"status" = $1`) + assert.Contains(t, q.sql, `"age" = $2`) + assert.Contains(t, q.sql, `"city" = $3`) + assert.Contains(t, q.sql, `OR ("role" = $4)`) assert.Len(t, q.params, 4) } diff --git a/internal/core/count_exists_tosql_test.go b/internal/core/count_exists_tosql_test.go index 22346fb..3e6ebc8 100644 --- a/internal/core/count_exists_tosql_test.go +++ b/internal/core/count_exists_tosql_test.go @@ -64,13 +64,13 @@ func TestSelectQuery_ToSQL_WithWhere(t *testing.T) { { name: "postgres: WHERE with positional placeholder", dialect: "postgres", - wantSQL: `SELECT * FROM "users" WHERE "id"=$1`, + wantSQL: `SELECT * FROM "users" WHERE "id" = $1`, wantParams: []interface{}{1}, }, { name: "mysql: WHERE with positional placeholder", dialect: "mysql", - wantSQL: "SELECT * FROM `users` WHERE `id`=?", + wantSQL: "SELECT * FROM `users` WHERE `id` = ?", wantParams: []interface{}{1}, }, } @@ -100,7 +100,7 @@ func TestSelectQuery_ToSQL_WithMultipleConditions(t *testing.T) { ToSQL() assert.Contains(t, sql, `SELECT "id", "name" FROM "users"`) - assert.Contains(t, sql, `WHERE "status"=$1 AND "age">$2`) + assert.Contains(t, sql, `WHERE "status" = $1 AND "age" > $2`) assert.Contains(t, sql, `ORDER BY "name" ASC`) assert.Contains(t, sql, `LIMIT 10`) assert.Equal(t, []interface{}{1, 18}, params) @@ -153,7 +153,7 @@ func TestUpdateQuery_ToSQL_Postgres(t *testing.T) { assert.Contains(t, sql, `UPDATE "users" SET`) assert.Contains(t, sql, `"status" = $1`) - assert.Contains(t, sql, `WHERE "id"=$2`) + assert.Contains(t, sql, `WHERE "id" = $2`) assert.Equal(t, []interface{}{2, 1}, params) } @@ -168,7 +168,7 @@ func TestUpdateQuery_ToSQL_MySQL(t *testing.T) { assert.Contains(t, sql, "UPDATE `users` SET") assert.Contains(t, sql, "`name` = ?") - assert.Contains(t, sql, "WHERE `id`=?") + assert.Contains(t, sql, "WHERE `id` = ?") assert.Equal(t, []interface{}{"Alice", 5}, params) } @@ -196,7 +196,7 @@ func TestUpdateQuery_ToSQL_SQLite(t *testing.T) { assert.Contains(t, sql, `UPDATE "products" SET`) assert.Contains(t, sql, `"price" = ?`) - assert.Contains(t, sql, `WHERE "id"=?`) + assert.Contains(t, sql, `WHERE "id" = ?`) assert.Equal(t, []interface{}{99, 10}, params) } @@ -210,7 +210,7 @@ func TestDeleteQuery_ToSQL_Postgres(t *testing.T) { sql, params := qb.Delete("users").Where(Eq("id", 1)).ToSQL() - assert.Equal(t, `DELETE FROM "users" WHERE "id"=$1`, sql) + assert.Equal(t, `DELETE FROM "users" WHERE "id" = $1`, sql) assert.Equal(t, []interface{}{1}, params) } @@ -220,7 +220,7 @@ func TestDeleteQuery_ToSQL_MySQL(t *testing.T) { sql, params := qb.Delete("sessions").Where(Eq("user_id", 99)).ToSQL() - assert.Equal(t, "DELETE FROM `sessions` WHERE `user_id`=?", sql) + assert.Equal(t, "DELETE FROM `sessions` WHERE `user_id` = ?", sql) assert.Equal(t, []interface{}{99}, params) } @@ -254,8 +254,8 @@ func TestDeleteQuery_ToSQL_MultipleConditions(t *testing.T) { ToSQL() assert.Contains(t, sql, `DELETE FROM "events" WHERE`) - assert.Contains(t, sql, `"status"=$1`) - assert.Contains(t, sql, `"created_at"<$2`) + assert.Contains(t, sql, `"status" = $1`) + assert.Contains(t, sql, `"created_at" < $2`) assert.Equal(t, []interface{}{"archived", "2020-01-01"}, params) } @@ -374,7 +374,7 @@ func TestSelectQuery_Exists_SQL_Postgres(t *testing.T) { innerSQL, innerParams := innerQuery.buildSQL(db.dialect) existsSQL := "SELECT EXISTS(" + innerSQL + ")" - assert.Equal(t, `SELECT EXISTS(SELECT 1 FROM "users" WHERE "email"=$1)`, existsSQL) + assert.Equal(t, `SELECT EXISTS(SELECT 1 FROM "users" WHERE "email" = $1)`, existsSQL) assert.Equal(t, []interface{}{"alice@example.com"}, innerParams) } @@ -396,7 +396,7 @@ func TestSelectQuery_Exists_SQL_MySQL(t *testing.T) { innerSQL, innerParams := innerQuery.buildSQL(db.dialect) existsSQL := "SELECT EXISTS(" + innerSQL + ")" - assert.Equal(t, "SELECT EXISTS(SELECT 1 FROM `users` WHERE `id`=?)", existsSQL) + assert.Equal(t, "SELECT EXISTS(SELECT 1 FROM `users` WHERE `id` = ?)", existsSQL) assert.Equal(t, []interface{}{7}, innerParams) } @@ -424,7 +424,7 @@ func TestSelectQuery_Exists_SQL_WithJoin(t *testing.T) { assert.Contains(t, existsSQL, "SELECT EXISTS(") assert.Contains(t, existsSQL, "INNER JOIN") - assert.Contains(t, existsSQL, `"status"=$1`) + assert.Contains(t, existsSQL, `"status" = $1`) } func TestSelectQuery_Exists_SQL_SQLite(t *testing.T) { @@ -445,7 +445,7 @@ func TestSelectQuery_Exists_SQL_SQLite(t *testing.T) { innerSQL, innerParams := innerQuery.buildSQL(db.dialect) existsSQL := "SELECT EXISTS(" + innerSQL + ")" - assert.Equal(t, `SELECT EXISTS(SELECT 1 FROM "products" WHERE "sku"=?)`, existsSQL) + assert.Equal(t, `SELECT EXISTS(SELECT 1 FROM "products" WHERE "sku" = ?)`, existsSQL) assert.Equal(t, []interface{}{"ABC-123"}, innerParams) } diff --git a/internal/core/exists_test.go b/internal/core/exists_test.go index d5407ea..09ac4f5 100644 --- a/internal/core/exists_test.go +++ b/internal/core/exists_test.go @@ -86,8 +86,8 @@ func TestExists_WithHashExp(t *testing.T) { sql, args := exp.Build(dialect) // HashExp keys are sorted: status, user_id assert.Contains(t, sql, `EXISTS (`) - assert.Contains(t, sql, `"status"=?`) - assert.Contains(t, sql, `"user_id"=?`) + assert.Contains(t, sql, `"status" = ?`) + assert.Contains(t, sql, `"user_id" = ?`) assert.Contains(t, sql, ` AND `) assert.Equal(t, []interface{}{"active", 123}, args) } @@ -104,8 +104,8 @@ func TestExists_WithComplexExpression(t *testing.T) { sql, args := exp.Build(dialect) assert.Contains(t, sql, `EXISTS (`) - assert.Contains(t, sql, `"user_id"=?`) - assert.Contains(t, sql, `"amount">?`) + assert.Contains(t, sql, `"user_id" = ?`) + assert.Contains(t, sql, `"amount" > ?`) assert.Contains(t, sql, `) AND (`) assert.Equal(t, []interface{}{123, 100}, args) } @@ -121,7 +121,7 @@ func TestNotExists_WithComplexExpression(t *testing.T) { sql, args := exp.Build(dialect) assert.Contains(t, sql, `NOT EXISTS (`) - assert.Contains(t, sql, `"status"=?`) + assert.Contains(t, sql, `"status" = ?`) assert.Contains(t, sql, `) OR (`) assert.Equal(t, []interface{}{"pending", "failed"}, args) } diff --git a/internal/core/expression.go b/internal/core/expression.go index 51497d9..162e403 100644 --- a/internal/core/expression.go +++ b/internal/core/expression.go @@ -125,7 +125,7 @@ func buildHashExpValue(key string, value interface{}, dialect dialects.Dialect) return in.Build(dialect) default: - return col + "=?", []interface{}{value} + return col + " = ?", []interface{}{value} } } @@ -222,11 +222,11 @@ func (e *CompareExp) Build(dialect dialects.Dialect) (string, []interface{}) { // Handle Expression values if expr, ok := e.Value.(Expression); ok { sql, args := expr.Build(dialect) - return col + e.Operator + "(" + sql + ")", args + return col + " " + e.Operator + " (" + sql + ")", args } // Simple comparison - return col + e.Operator + "?", []interface{}{e.Value} + return col + " " + e.Operator + " ?", []interface{}{e.Value} } // InExp represents an IN or NOT IN expression. @@ -298,9 +298,9 @@ func buildInExpSingleValue(col string, val interface{}, not bool, dialect dialec } // Non-NULL single value if not { - return col + "<>?", []interface{}{val}, true + return col + " <> ?", []interface{}{val}, true } - return col + "=?", []interface{}{val}, true + return col + " = ?", []interface{}{val}, true } // Build converts an IN expression into a SQL fragment. diff --git a/internal/core/expression_test.go b/internal/core/expression_test.go index f228ab5..35ee1c3 100644 --- a/internal/core/expression_test.go +++ b/internal/core/expression_test.go @@ -83,21 +83,21 @@ func TestHashExp_Build(t *testing.T) { name: "single value postgres", dialect: "postgres", hash: HashExp{"status": 1}, - wantSQL: `"status"=?`, + wantSQL: `"status" = ?`, wantArgs: []interface{}{1}, }, { name: "single value mysql", dialect: "mysql", hash: HashExp{"status": 1}, - wantSQL: "`status`=?", + wantSQL: "`status` = ?", wantArgs: []interface{}{1}, }, { name: "single value sqlite", dialect: "sqlite", hash: HashExp{"status": 1}, - wantSQL: `"status"=?`, + wantSQL: `"status" = ?`, wantArgs: []interface{}{1}, }, { @@ -108,7 +108,7 @@ func TestHashExp_Build(t *testing.T) { "age": 18, "role": "admin", }, - wantSQL: `"age"=? AND "role"=? AND "status"=?`, + wantSQL: `"age" = ? AND "role" = ? AND "status" = ?`, wantArgs: []interface{}{18, "admin", 1}, // sorted by keys }, { @@ -118,7 +118,7 @@ func TestHashExp_Build(t *testing.T) { "deleted_at": nil, "status": 1, }, - wantSQL: `"deleted_at" IS NULL AND "status"=?`, + wantSQL: `"deleted_at" IS NULL AND "status" = ?`, wantArgs: []interface{}{1}, }, { @@ -128,7 +128,7 @@ func TestHashExp_Build(t *testing.T) { "age": []interface{}{18, 19, 20}, "status": 1, }, - wantSQL: `"age" IN (?, ?, ?) AND "status"=?`, + wantSQL: `"age" IN (?, ?, ?) AND "status" = ?`, wantArgs: []interface{}{18, 19, 20, 1}, }, { @@ -138,7 +138,7 @@ func TestHashExp_Build(t *testing.T) { "age": []interface{}{18, 19, 20}, "status": 1, }, - wantSQL: "`age` IN (?, ?, ?) AND `status`=?", + wantSQL: "`age` IN (?, ?, ?) AND `status` = ?", wantArgs: []interface{}{18, 19, 20, 1}, }, { @@ -157,7 +157,7 @@ func TestHashExp_Build(t *testing.T) { "age": Eq("age", 18), "status": 1, }, - wantSQL: `("age"=?) AND "status"=?`, + wantSQL: `("age" = ?) AND "status" = ?`, wantArgs: []interface{}{18, 1}, }, } @@ -185,21 +185,21 @@ func TestCompareExp_Build(t *testing.T) { name: "Eq postgres", dialect: "postgres", exp: Eq("status", 1), - wantSQL: `"status"=?`, + wantSQL: `"status" = ?`, wantArgs: []interface{}{1}, }, { name: "Eq mysql", dialect: "mysql", exp: Eq("status", 1), - wantSQL: "`status`=?", + wantSQL: "`status` = ?", wantArgs: []interface{}{1}, }, { name: "Eq sqlite", dialect: "sqlite", exp: Eq("status", 1), - wantSQL: `"status"=?`, + wantSQL: `"status" = ?`, wantArgs: []interface{}{1}, }, { @@ -213,7 +213,7 @@ func TestCompareExp_Build(t *testing.T) { name: "NotEq postgres", dialect: "postgres", exp: NotEq("status", 0), - wantSQL: `"status"<>?`, + wantSQL: `"status" <> ?`, wantArgs: []interface{}{0}, }, { @@ -227,28 +227,28 @@ func TestCompareExp_Build(t *testing.T) { name: "GreaterThan postgres", dialect: "postgres", exp: GreaterThan("age", 18), - wantSQL: `"age">?`, + wantSQL: `"age" > ?`, wantArgs: []interface{}{18}, }, { name: "LessThan mysql", dialect: "mysql", exp: LessThan("age", 65), - wantSQL: "`age`=?`, + wantSQL: `"score" >= ?`, wantArgs: []interface{}{80}, }, { name: "LessOrEqual postgres", dialect: "postgres", exp: LessOrEqual("price", 100.50), - wantSQL: `"price"<=?`, + wantSQL: `"price" <= ?`, wantArgs: []interface{}{100.50}, }, } @@ -283,7 +283,7 @@ func TestInExp_Build(t *testing.T) { name: "IN single value postgres", dialect: "postgres", exp: In("status", 1), - wantSQL: `"status"=?`, + wantSQL: `"status" = ?`, wantArgs: []interface{}{1}, }, { @@ -325,7 +325,7 @@ func TestInExp_Build(t *testing.T) { name: "NOT IN single value postgres", dialect: "postgres", exp: NotIn("status", 0), - wantSQL: `"status"<>?`, + wantSQL: `"status" <> ?`, wantArgs: []interface{}{0}, }, { @@ -493,7 +493,7 @@ func TestAndOrExp_Build(t *testing.T) { name: "AND single expression", dialect: "postgres", exp: And(Eq("status", 1)), - wantSQL: `"status"=?`, + wantSQL: `"status" = ?`, wantArgs: []interface{}{1}, }, { @@ -504,7 +504,7 @@ func TestAndOrExp_Build(t *testing.T) { GreaterThan("age", 18), Like("name", "john"), ), - wantSQL: `("status"=?) AND ("age">?) AND ("name" LIKE ?)`, + wantSQL: `("status" = ?) AND ("age" > ?) AND ("name" LIKE ?)`, wantArgs: []interface{}{1, 18, "%john%"}, }, { @@ -514,7 +514,7 @@ func TestAndOrExp_Build(t *testing.T) { Eq("role", "admin"), Eq("role", "moderator"), ), - wantSQL: "(`role`=?) OR (`role`=?)", + wantSQL: "(`role` = ?) OR (`role` = ?)", wantArgs: []interface{}{"admin", "moderator"}, }, { @@ -525,7 +525,7 @@ func TestAndOrExp_Build(t *testing.T) { nil, GreaterThan("age", 18), ), - wantSQL: `("status"=?) AND ("age">?)`, + wantSQL: `("status" = ?) AND ("age" > ?)`, wantArgs: []interface{}{1, 18}, }, { @@ -538,7 +538,7 @@ func TestAndOrExp_Build(t *testing.T) { Eq("role", "moderator"), ), ), - wantSQL: `("active"=?) AND (("role"=?) OR ("role"=?))`, + wantSQL: `("active" = ?) AND (("role" = ?) OR ("role" = ?))`, wantArgs: []interface{}{true, "admin", "moderator"}, }, } @@ -573,7 +573,7 @@ func TestNotExp_Build(t *testing.T) { name: "NOT simple expression postgres", dialect: "postgres", exp: Not(Eq("active", true)), - wantSQL: `NOT ("active"=?)`, + wantSQL: `NOT ("active" = ?)`, wantArgs: []interface{}{true}, }, { @@ -590,7 +590,7 @@ func TestNotExp_Build(t *testing.T) { Eq("deleted", false), GreaterThan("age", 18), )), - wantSQL: `NOT (("deleted"=?) AND ("age">?))`, + wantSQL: `NOT (("deleted" = ?) AND ("age" > ?))`, wantArgs: []interface{}{false, 18}, }, } @@ -638,7 +638,7 @@ func TestCompareExp_WithExpressionValue(t *testing.T) { exp := Eq("total", NewExp("(SELECT SUM(amount) FROM orders WHERE user_id = ?)", 123)) sql, args := exp.Build(dialect) - assert.Equal(t, `"total"=((SELECT SUM(amount) FROM orders WHERE user_id = ?))`, sql) + assert.Equal(t, `"total" = ((SELECT SUM(amount) FROM orders WHERE user_id = ?))`, sql) assert.Equal(t, []interface{}{123}, args) } @@ -697,7 +697,7 @@ func TestCompareExp_TableAlias(t *testing.T) { name: "Eq table.column with value", dialect: "postgres", exp: Eq("u.status", 1), - wantSQL: `"u"."status"=?`, + wantSQL: `"u"."status" = ?`, wantArgs: []interface{}{1}, }, { @@ -711,35 +711,35 @@ func TestCompareExp_TableAlias(t *testing.T) { name: "GreaterThan table.column", dialect: "postgres", exp: GreaterThan("u.age", 18), - wantSQL: `"u"."age">?`, + wantSQL: `"u"."age" > ?`, wantArgs: []interface{}{18}, }, { name: "LessThan table.column", dialect: "mysql", exp: LessThan("o.amount", 100), - wantSQL: "`o`.`amount`=?`, + wantSQL: `"p"."price" >= ?`, wantArgs: []interface{}{9.99}, }, { name: "LessOrEqual table.column", dialect: "sqlite", exp: LessOrEqual("t.score", 50), - wantSQL: `"t"."score"<=?`, + wantSQL: `"t"."score" <= ?`, wantArgs: []interface{}{50}, }, { name: "Eq with Expression value and table alias", dialect: "postgres", exp: Eq("m.user_id", NewExp("u.id")), - wantSQL: `"m"."user_id"=(u.id)`, + wantSQL: `"m"."user_id" = (u.id)`, wantArgs: nil, }, } @@ -788,7 +788,7 @@ func TestInExp_TableAlias(t *testing.T) { name: "In table.column single value optimization", dialect: "postgres", exp: In("u.id", 42), - wantSQL: `"u"."id"=?`, + wantSQL: `"u"."id" = ?`, wantArgs: []interface{}{42}, }, { @@ -921,7 +921,7 @@ func TestHashExp_TableAlias(t *testing.T) { name: "single table.column value mysql", dialect: "mysql", hash: HashExp{"u.status": 1}, - wantSQL: "`u`.`status`=?", + wantSQL: "`u`.`status` = ?", wantArgs: []interface{}{1}, }, { @@ -931,7 +931,7 @@ func TestHashExp_TableAlias(t *testing.T) { "c.deleted_at": nil, "c.status": "active", }, - wantSQL: `"c"."deleted_at" IS NULL AND "c"."status"=?`, + wantSQL: `"c"."deleted_at" IS NULL AND "c"."status" = ?`, wantArgs: []interface{}{"active"}, }, { @@ -963,7 +963,7 @@ func TestTableAlias_ComposedExpressions(t *testing.T) { GreaterThan("c.revenue", 1000), ) sql, args := exp.Build(d) - assert.Equal(t, `("c"."deleted_at" IS NULL) AND ("c"."revenue">?)`, sql) + assert.Equal(t, `("c"."deleted_at" IS NULL) AND ("c"."revenue" > ?)`, sql) assert.Equal(t, []interface{}{1000}, args) }) @@ -973,7 +973,7 @@ func TestTableAlias_ComposedExpressions(t *testing.T) { Eq("u.role", "superadmin"), ) sql, args := exp.Build(d) - assert.Equal(t, `("u"."role"=?) OR ("u"."role"=?)`, sql) + assert.Equal(t, `("u"."role" = ?) OR ("u"."role" = ?)`, sql) assert.Equal(t, []interface{}{"admin", "superadmin"}, args) }) @@ -993,7 +993,7 @@ func TestTableAlias_ComposedExpressions(t *testing.T) { ), ) sql, args := exp.Build(d) - assert.Equal(t, `("c"."deleted_at" IS NULL) AND (("o"."total">?) OR ("o"."status" IN (?, ?)))`, sql) + assert.Equal(t, `("c"."deleted_at" IS NULL) AND (("o"."total" > ?) OR ("o"."status" IN (?, ?)))`, sql) assert.Equal(t, []interface{}{500, "vip", "premium"}, args) }) } @@ -1011,15 +1011,15 @@ func TestHashExp_AllDialects(t *testing.T) { }{ { dialectName: "postgres", - wantSQL: `"age" IN (?, ?, ?) AND "status"=?`, + wantSQL: `"age" IN (?, ?, ?) AND "status" = ?`, }, { dialectName: "mysql", - wantSQL: "`age` IN (?, ?, ?) AND `status`=?", + wantSQL: "`age` IN (?, ?, ?) AND `status` = ?", }, { dialectName: "sqlite", - wantSQL: `"age" IN (?, ?, ?) AND "status"=?`, + wantSQL: `"age" IN (?, ?, ?) AND "status" = ?`, }, } diff --git a/internal/core/func_expressions.go b/internal/core/func_expressions.go index 0d15dcd..10e0ede 100644 --- a/internal/core/func_expressions.go +++ b/internal/core/func_expressions.go @@ -47,6 +47,9 @@ func Case(column string) *CaseExp { // CaseWhen creates a searched CASE expression (without column). // +// WARNING: String conditions are interpolated as raw SQL. Never pass user input +// as the condition argument. Use parameterized values for the result (THEN) only. +// // Example: // // relica.CaseWhen(). diff --git a/internal/core/scanner.go b/internal/core/scanner.go index d55e79f..ed50f56 100644 --- a/internal/core/scanner.go +++ b/internal/core/scanner.go @@ -134,6 +134,9 @@ func (s *scanner) scanRow(rows *sql.Rows, dest interface{}) error { if destValue.Kind() != reflect.Pointer { return fmt.Errorf("scanner: dest must be pointer to struct, got %T", dest) } + if destValue.IsNil() { + return fmt.Errorf("scanner: dest is a nil pointer, pass a non-nil *%s", destValue.Type().Elem()) + } destValue = destValue.Elem() if destValue.Kind() != reflect.Struct { diff --git a/internal/core/subquery_test.go b/internal/core/subquery_test.go index 39e565b..1bf68c6 100644 --- a/internal/core/subquery_test.go +++ b/internal/core/subquery_test.go @@ -422,7 +422,7 @@ func TestInExp_Single_Regular_Value(t *testing.T) { exp := In("id", 123) sql, args := exp.Build(dialect) - assert.Equal(t, `"id"=?`, sql) + assert.Equal(t, `"id" = ?`, sql) assert.Equal(t, []interface{}{123}, args) }