Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion reader_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,19 @@ func getReaderBase(ctx context.Context, query dal.Query, execute executeQueryFun
args := q.Args()
a = make([]any, len(args))
for i, arg := range args {
a[i] = arg
// dal.QueryArg{Name, Value} is dalgo's own bind-value shape, not
// something database/sql understands directly — passing the
// struct itself as a[i] fails every call with a non-empty Args()
// ("unsupported type dal.QueryArg, a struct"). A named arg
// (Name != "") becomes a sql.NamedArg via sql.Named, so an
// @name/:name/$name placeholder in the query text binds by
// name; a positional arg (Name == "") passes its Value straight
// through for ordinary ?/$N placeholders.
if arg.Name != "" {
a[i] = sql.Named(arg.Name, arg.Value)
} else {
a[i] = arg.Value
}
}
case dal.StructuredQuery:
// emitSQL rewrites dalgo's `SELECT TOP N` into ANSI `LIMIT N`
Expand Down
105 changes: 105 additions & 0 deletions reader_base_textquery_args_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package dalgo2sql

import (
"context"
"testing"

"github.com/dal-go/dalgo/dal"
)

// TestGetReaderBase_TextQuery_PositionalArgs proves a positional
// dal.QueryArg{Value: ...} (no Name) reaches database/sql as its plain
// Value, not the dal.QueryArg struct itself — against a real, in-memory
// SQLite database (modernc.org/sqlite), not a mock, so a wrong bind value
// is a genuine driver-level error, not a mock-expectation mismatch.
func TestGetReaderBase_TextQuery_PositionalArgs(t *testing.T) {
db := openTestSQLiteDB(t, `CREATE TABLE widgets (id INTEGER PRIMARY KEY, name TEXT)`)
if _, err := db.Exec(`INSERT INTO widgets (id, name) VALUES (1, 'bolt'), (2, 'nut')`); err != nil {
t.Fatalf("seed insert: %v", err)
}

query := dal.NewTextQuery("SELECT name FROM widgets WHERE id = ?", nil, dal.QueryArg{Value: 2})
rb, err := getReaderBase(context.Background(), query, db.QueryContext)
if err != nil {
t.Fatalf("getReaderBase: %v", err)
}
defer func() { _ = rb.rows.Close() }()

if !rb.rows.Next() {
t.Fatalf("expected one row, got none (err: %v)", rb.rows.Err())
}
var name string
if err := rb.rows.Scan(&name); err != nil {
t.Fatalf("Scan: %v", err)
}
if name != "nut" {
t.Errorf("name = %q, want %q", name, "nut")
}
}

// TestGetReaderBase_TextQuery_NamedArgs proves a named
// dal.QueryArg{Name: "id", Value: ...} reaches database/sql as a proper
// sql.NamedArg (via sql.Named), so a native SQL query written with an
// @-prefixed placeholder — the shape pkg/secureread.RunNativeSQL's own
// callers use — actually binds, instead of every row silently matching
// (or the driver rejecting the query outright) because the whole
// dal.QueryArg struct was passed as the bind value.
func TestGetReaderBase_TextQuery_NamedArgs(t *testing.T) {
db := openTestSQLiteDB(t, `CREATE TABLE widgets (id INTEGER PRIMARY KEY, name TEXT)`)
if _, err := db.Exec(`INSERT INTO widgets (id, name) VALUES (1, 'bolt'), (2, 'nut')`); err != nil {
t.Fatalf("seed insert: %v", err)
}

query := dal.NewTextQuery("SELECT name FROM widgets WHERE id = @id", nil, dal.QueryArg{Name: "id", Value: 2})
rb, err := getReaderBase(context.Background(), query, db.QueryContext)
if err != nil {
t.Fatalf("getReaderBase: %v", err)
}
defer func() { _ = rb.rows.Close() }()

if !rb.rows.Next() {
t.Fatalf("expected one row, got none (err: %v)", rb.rows.Err())
}
var name string
if err := rb.rows.Scan(&name); err != nil {
t.Fatalf("Scan: %v", err)
}
if name != "nut" {
t.Errorf("name = %q, want %q", name, "nut")
}

if rb.rows.Next() {
t.Errorf("expected exactly one row, got a second one — the @id filter did not apply")
}
}

// TestGetReaderBase_TextQuery_MixedArgs proves a query mixing a named and a
// positional arg binds both correctly at once.
func TestGetReaderBase_TextQuery_MixedArgs(t *testing.T) {
db := openTestSQLiteDB(t, `CREATE TABLE widgets (id INTEGER PRIMARY KEY, name TEXT, qty INTEGER)`)
if _, err := db.Exec(`INSERT INTO widgets (id, name, qty) VALUES (1, 'bolt', 5), (2, 'nut', 5), (3, 'nut', 9)`); err != nil {
t.Fatalf("seed insert: %v", err)
}

query := dal.NewTextQuery(
"SELECT id FROM widgets WHERE name = @name AND qty = ?", nil,
dal.QueryArg{Name: "name", Value: "nut"},
dal.QueryArg{Value: 5},
)
rb, err := getReaderBase(context.Background(), query, db.QueryContext)
if err != nil {
t.Fatalf("getReaderBase: %v", err)
}
defer func() { _ = rb.rows.Close() }()

if !rb.rows.Next() {
t.Fatalf("expected one row, got none (err: %v)", rb.rows.Err())
}
var id int
if err := rb.rows.Scan(&id); err != nil {
t.Fatalf("Scan: %v", err)
}
if id != 2 {
t.Errorf("id = %d, want 2", id)
}
}