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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ db.QueryContext(ctx, query, args...)
The following database placeholders are supported:
- `?` (used by MySQL and SQLite)
- `$1`, `$2`, ..., `$N` (used by PostgreSQL)
- `@p1`, `@p2`, ..., `@pN` (used by MSSQL)
- `@p1`, `@p2`, ..., `@pN` (used by Microsoft SQL Server)
- `:1`, `:2`, ..., `:N` (used by Oracle Database):

### Scanner

Expand Down Expand Up @@ -105,6 +106,7 @@ Integration tests cover the following databases and drivers:
- MySQL with [go-sql-driver/mysql][5]
- SQLite with [modernc.org/sqlite][6]
- Microsoft SQL Server with [microsoft/go-mssqldb][7]
- Oracle Database with [sijms/go-ora][8]

See [integration_test.go](tests/integration_test.go) for details.

Expand All @@ -120,3 +122,4 @@ See [integration_test.go](tests/integration_test.go) for details.
[5]: https://github.com/go-sql-driver/mysql
[6]: https://gitlab.com/cznic/sqlite
[7]: https://github.com/microsoft/go-mssqldb
[8]: https://github.com/sijms/go-ora
6 changes: 5 additions & 1 deletion builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Builder struct {
// | MySQL, MariaDB, SQLite | %? | ? |
// | PostgreSQL | %$ | $N |
// | Microsoft SQL Server | %@ | @pN |
// | Oracle Database | %: | :N |
// -----------------------------------------------
//
// Here, N is an auto-incrementing counter.
Expand Down Expand Up @@ -68,7 +69,7 @@ type formatter struct {
// Format implements [fmt.Formatter].
func (f formatter) Format(s fmt.State, verb rune) {
switch verb {
case '?', '$', '@':
case '?', '$', '@', ':':
if f.builder.placeholder == 0 {
f.builder.placeholder = verb
}
Expand Down Expand Up @@ -96,6 +97,9 @@ func appendOne(w io.Writer, b *Builder, verb rune, arg any) {
case '@':
b.counter++
fmt.Fprintf(w, "@p%d", b.counter)
case ':':
b.counter++
fmt.Fprintf(w, ":%d", b.counter)
}
b.args = append(b.args, arg)
}
Expand Down
4 changes: 4 additions & 0 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ func TestBuilder_dialects(t *testing.T) {
format: "SELECT * FROM tbl WHERE foo = %@ AND bar = %@ AND baz = %@",
query: "SELECT * FROM tbl WHERE foo = @p1 AND bar = @p2 AND baz = @p3",
},
":": {
format: "SELECT * FROM tbl WHERE foo = %: AND bar = %: AND baz = %:",
query: "SELECT * FROM tbl WHERE foo = :1 AND bar = :2 AND baz = :3",
},
}

for name, test := range tests {
Expand Down
7 changes: 7 additions & 0 deletions tests/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ services:
environment:
- ACCEPT_EULA=Y
- MSSQL_SA_PASSWORD=root+1234
oracle:
# https://hub.docker.com/r/gvenzl/oracle-free
image: gvenzl/oracle-free:latest-faststart
ports:
- 1521:1521
environment:
- ORACLE_PASSWORD=root
1 change: 1 addition & 0 deletions tests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/jackc/pgx/v5 v5.7.3
github.com/lib/pq v1.10.9
github.com/microsoft/go-mssqldb v1.9.3
github.com/sijms/go-ora/v2 v2.9.0
go-simpler.org/assert v0.9.0
go-simpler.org/queries v0.1.0
modernc.org/sqlite v1.38.0
Expand Down
2 changes: 2 additions & 0 deletions tests/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/sijms/go-ora/v2 v2.9.0 h1:+iQbUeTeCOFMb5BsOMgUhV8KWyrv9yjKpcK4x7+MFrg=
github.com/sijms/go-ora/v2 v2.9.0/go.mod h1:QgFInVi3ZWyqAiJwzBQA+nbKYKH77tdp1PYoCqhR2dU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
Expand Down
232 changes: 124 additions & 108 deletions tests/integration_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tests

import (
"cmp"
"context"
"database/sql"
"database/sql/driver"
Expand All @@ -10,32 +11,33 @@ import (
"github.com/go-sql-driver/mysql"
pgx "github.com/jackc/pgx/v5/stdlib"
"github.com/lib/pq"
mssql "github.com/microsoft/go-mssqldb"
mssqldb "github.com/microsoft/go-mssqldb"
ora "github.com/sijms/go-ora/v2"
"go-simpler.org/assert"
. "go-simpler.org/assert/EF"
"go-simpler.org/queries"
"modernc.org/sqlite"
)

// ----------------------------------------------------------------------------------------------------------------------
// | Interface / Driver | lib/pq | jackc/pgx | go-sql-driver/mysql | modernc.org/sqlite | microsoft/go-mssqldb |
// |-----------------------------|--------|-----------|---------------------|--------------------|----------------------|
// | [driver.DriverContext] | - | + | + | - | - |
// | [driver.Pinger] | + | + | + | + | + |
// | [driver.ExecerContext] | + | + | + | + | - |
// | [driver.QueryerContext] | + | + | + | + | - |
// | [driver.ConnPrepareContext] | + | + | + | + | + |
// | [driver.ConnBeginTx] | + | + | + | + | + |
// | [driver.SessionResetter] | + | + | + | + | + |
// | [driver.Validator] | + | - | + | + | + |
// | [driver.NamedValueChecker] | - | + | + | - | + |
// ----------------------------------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------------------------------------------
// | Interface / Driver | lib/pq | jackc/pgx | go-sql-driver/mysql | modernc.org/sqlite | microsoft/go-mssqldb | sijms/go-ora |
// |-----------------------------|--------|-----------|---------------------|--------------------|----------------------|--------------|
// | [driver.DriverContext] | - | + | + | - | - | + |
// | [driver.Pinger] | + | + | + | + | + | + |
// | [driver.ExecerContext] | + | + | + | + | - | + |
// | [driver.QueryerContext] | + | + | + | + | - | + |
// | [driver.ConnPrepareContext] | + | + | + | + | + | + |
// | [driver.ConnBeginTx] | + | + | + | + | + | + |
// | [driver.SessionResetter] | + | + | + | + | + | + |
// | [driver.Validator] | + | - | + | + | + | - |
// | [driver.NamedValueChecker] | - | + | + | - | + | + |
// -------------------------------------------------------------------------------------------------------------------------------------
//
// See https://go.dev/wiki/SQLDrivers for the full list of drivers.
var databases = map[string]struct {
dataSourceName string
insertFixturesQuery string
drivers map[string]driver.Driver
dataSourceName string
insertFixturesQueryFormat string
drivers map[string]driver.Driver
}{
"postgres": {
"postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable",
Expand All @@ -56,14 +58,21 @@ var databases = map[string]struct {
"test.sqlite",
"INSERT INTO users (id, name) VALUES (%?, %?), (%?, %?)",
map[string]driver.Driver{
"modernc.org/sqlite": new(sqlite.Driver), // https://gitlab.com/cznic/sqlite ,
"modernc.org/sqlite": new(sqlite.Driver), // https://gitlab.com/cznic/sqlite
},
},
"mssql": {
"sqlserver://sa:root+1234@localhost:1433/msdb",
"INSERT INTO users (id, name) VALUES (%@, %@), (%@, %@)",
map[string]driver.Driver{
"microsoft/go-mssqldb": new(mssql.Driver), // https://github.com/microsoft/go-mssqldb ,
"microsoft/go-mssqldb": new(mssqldb.Driver), // https://github.com/microsoft/go-mssqldb
},
},
"oracle": {
"oracle://sys:root@localhost:1521/freepdb1",
"INSERT INTO users (id, name) VALUES (%:, %:), (%:, %:)",
map[string]driver.Driver{
"sijms/go-ora": new(ora.OracleDriver), // https://github.com/sijms/go-ora
},
},
}
Expand All @@ -74,113 +83,120 @@ func TestIntegration(t *testing.T) {
type dto struct {
ID int `sql:"id"`
Name string `sql:"name"`

// In Oracle, all columns are uppercase.
ID2 int `sql:"ID"`
Name2 string `sql:"NAME"`
}
table := []dto{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
}

for _, params := range databases {
for driverName, driverIface := range params.drivers {
var execCalls int
var queryCalls int
var prepareCalls int

interceptor := queries.Interceptor{
Driver: driverIface,
ExecContext: func(ctx context.Context, query string, args []driver.NamedValue, execer driver.ExecerContext) (driver.Result, error) {
execCalls++
t.Logf("[%s] ExecContext: %s %v", driverName, query, namedToAny(args))
return execer.ExecContext(ctx, query, args)
},
QueryContext: func(ctx context.Context, query string, args []driver.NamedValue, queryer driver.QueryerContext) (driver.Rows, error) {
queryCalls++
t.Logf("[%s] QueryContext: %s %v", driverName, query, namedToAny(args))
return queryer.QueryContext(ctx, query, args)
},
PrepareContext: func(ctx context.Context, query string, preparer driver.ConnPrepareContext) (driver.Stmt, error) {
prepareCalls++
t.Logf("[%s] PrepareContext: %s", driverName, query)
return preparer.PrepareContext(ctx, query)
},
}

sql.Register(driverName, interceptor)
db, err := sql.Open(driverName, params.dataSourceName)
assert.NoErr[F](t, err)
defer db.Close()

// wait until the database is ready.
for attempt := 0; ; attempt++ {
err := db.PingContext(ctx)
if err == nil {
break
}
if attempt == 10 {
t.Fatal(err)
for dbName, dbParams := range databases {
for driverName, driverIface := range dbParams.drivers {
t.Run(dbName+"+"+driverName, func(t *testing.T) {
var execCalls int
var queryCalls int
var prepareCalls int

interceptor := queries.Interceptor{
Driver: driverIface,
ExecContext: func(ctx context.Context, query string, args []driver.NamedValue, execer driver.ExecerContext) (driver.Result, error) {
execCalls++
t.Logf("ExecContext: %s %v", query, namedToAny(args))
return execer.ExecContext(ctx, query, args)
},
QueryContext: func(ctx context.Context, query string, args []driver.NamedValue, queryer driver.QueryerContext) (driver.Rows, error) {
queryCalls++
t.Logf("QueryContext: %s %v", query, namedToAny(args))
return queryer.QueryContext(ctx, query, args)
},
PrepareContext: func(ctx context.Context, query string, preparer driver.ConnPrepareContext) (driver.Stmt, error) {
prepareCalls++
t.Logf("PrepareContext: %s", query)
return preparer.PrepareContext(ctx, query)
},
}
time.Sleep(time.Second)
}

_, err = db.ExecContext(ctx, "CREATE TABLE users (id INTEGER PRIMARY KEY, name VARCHAR(8))")
assert.NoErr[F](t, err)
driverName += "+interceptor"
sql.Register(driverName, interceptor)

query, args := queries.Build(params.insertFixturesQuery,
table[0].ID, table[0].Name,
table[1].ID, table[1].Name,
)
_, err = db.ExecContext(ctx, query, args...)
assert.NoErr[F](t, err)

tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
assert.NoErr[F](t, err)
defer tx.Rollback()

for _, queryer := range []queries.Queryer{db, tx} {
_, err := queries.QueryRow[string](ctx, queryer, "SELECT name FROM users WHERE id = 0")
assert.IsErr[E](t, err, sql.ErrNoRows)
db, err := sql.Open(driverName, dbParams.dataSourceName)
assert.NoErr[F](t, err)
defer db.Close()

for attempt := 0; ; attempt++ {
err := db.PingContext(ctx)
if err == nil {
break
}
if attempt == 10 {
t.Fatal(err)
}
time.Sleep(time.Second)
}

name, err := queries.QueryRow[string](ctx, queryer, "SELECT name FROM users WHERE id = 1")
_, err = db.ExecContext(ctx, "CREATE TABLE users (id INTEGER PRIMARY KEY, name VARCHAR(8))")
assert.NoErr[F](t, err)
assert.Equal[E](t, name, table[0].Name)

names, err := queries.Collect(queries.Query[string](ctx, queryer, "SELECT name FROM users"))
query, args := queries.Build(dbParams.insertFixturesQueryFormat,
table[0].ID, table[0].Name,
table[1].ID, table[1].Name,
)
_, err = db.ExecContext(ctx, query, args...)
assert.NoErr[F](t, err)
assert.Equal[E](t, names, []string{table[0].Name, table[1].Name})

user, err := queries.QueryRow[dto](ctx, queryer, "SELECT id, name FROM users WHERE id = 1")
tx, err := db.BeginTx(ctx, nil)
assert.NoErr[F](t, err)
assert.Equal[E](t, user.ID, table[0].ID)
assert.Equal[E](t, user.Name, table[0].Name)
defer tx.Rollback()

for _, queryer := range []queries.Queryer{db, tx} {
_, err := queries.QueryRow[string](ctx, queryer, "SELECT name FROM users WHERE id = 0")
assert.IsErr[E](t, err, sql.ErrNoRows)

var i int
for user, err := range queries.Query[dto](ctx, queryer, "SELECT id, name FROM users ORDER BY id") {
name, err := queries.QueryRow[string](ctx, queryer, "SELECT name FROM users WHERE id = 1")
assert.NoErr[F](t, err)
assert.Equal[E](t, user.ID, table[i].ID)
assert.Equal[E](t, user.Name, table[i].Name)
i++
assert.Equal[E](t, name, table[0].Name)

names, err := queries.Collect(queries.Query[string](ctx, queryer, "SELECT name FROM users"))
assert.NoErr[F](t, err)
assert.Equal[E](t, names, []string{table[0].Name, table[1].Name})

user, err := queries.QueryRow[dto](ctx, queryer, "SELECT id, name FROM users WHERE id = 1")
assert.NoErr[F](t, err)
assert.Equal[E](t, cmp.Or(user.ID, user.ID2), table[0].ID)
assert.Equal[E](t, cmp.Or(user.Name, user.Name2), table[0].Name)

var i int
for user, err := range queries.Query[dto](ctx, queryer, "SELECT id, name FROM users ORDER BY id") {
assert.NoErr[F](t, err)
assert.Equal[E](t, cmp.Or(user.ID, user.ID2), table[i].ID)
assert.Equal[E](t, cmp.Or(user.Name, user.Name2), table[i].Name)
i++
}
}

assert.NoErr[F](t, tx.Commit())

_, err = db.ExecContext(ctx, "DROP TABLE users")
assert.NoErr[F](t, err)

switch db.Driver().(type) {
case *mysql.MySQLDriver: // falls back to PrepareContext for queries with arguments.
assert.Equal[E](t, execCalls, 3)
assert.Equal[E](t, queryCalls, 5*2)
assert.Equal[E](t, prepareCalls, 1)
case *mssqldb.Driver: // always uses PrepareContext.
assert.Equal[E](t, execCalls, 0)
assert.Equal[E](t, queryCalls, 0)
assert.Equal[E](t, prepareCalls, 3+5*2)
default:
assert.Equal[E](t, execCalls, 3)
assert.Equal[E](t, queryCalls, 5*2)
assert.Equal[E](t, prepareCalls, 0)
}
}

assert.NoErr[F](t, tx.Commit())

_, err = db.ExecContext(ctx, "DROP TABLE users")
assert.NoErr[F](t, err)

switch db.Driver().(type) {
case *mysql.MySQLDriver: // falls back to PrepareContext for queries with arguments.
assert.Equal[E](t, execCalls, 3)
assert.Equal[E](t, queryCalls, 5*2)
assert.Equal[E](t, prepareCalls, 1)
case *mssql.Driver: // always uses PrepareContext.
assert.Equal[E](t, execCalls, 0)
assert.Equal[E](t, queryCalls, 0)
assert.Equal[E](t, prepareCalls, 3+5*2)
default:
assert.Equal[E](t, execCalls, 3)
assert.Equal[E](t, queryCalls, 5*2)
assert.Equal[E](t, prepareCalls, 0)
}
})
}
}
}
Expand Down
Loading