From 6cad7ac72cbdaf8cf0950513ceb90120426e9bc7 Mon Sep 17 00:00:00 2001 From: Mario L Gutierrez Date: Thu, 25 Jun 2015 18:31:39 -0700 Subject: [PATCH] try delete cache key on error; bug in remapPlaceholders --- CHANGES.md | 12 +++- Gododir/generate.go | 6 +- Gododir/main.go | 42 ++++++++++++- README.md | 137 +++++++++++++++++-------------------------- builder.go | 10 ---- execer.go | 2 +- generate.go | 5 -- insert.go | 2 +- interpolate.go | 6 +- interpolate_test.go | 4 +- kvs/interfaces.go | 2 +- postgres/postgres.go | 5 +- select_doc_test.go | 78 +++++++++--------------- sqlx-runner/db.go | 1 - sqlx-runner/exec.go | 86 ++++++++++++++------------- types.go | 10 ++-- where.go | 37 ++++++------ 17 files changed, 217 insertions(+), 228 deletions(-) delete mode 100644 generate.go diff --git a/CHANGES.md b/CHANGES.md index 366306b..ae95304 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,14 @@ -# Changes from legacy to v1 +## v1.1.0 + +* [Caching](https://github.com/mgutz/dat#caching) - caching with Redis or (in-memory for testing) +* [LogQueriesThreshold](https://github.com/mgutz/dat#tracing-sql) - log slow queries +* dat.Null* creators +* fix resource cleanup +* fix duplicate error logging +* include RFC339Nano in NullTime parsing +* HUGE BUG in remapPlaceholders + +## v1.0.0 * Original dat moved to legacy branch. diff --git a/Gododir/generate.go b/Gododir/generate.go index 034f134..d073a93 100644 --- a/Gododir/generate.go +++ b/Gododir/generate.go @@ -35,7 +35,7 @@ package dat func generateTasks(p *do.Project) { p.Task("builder-boilerplate", nil, func(c *do.Context) { context := do.M{ - "builders": []string{"DeleteBuilder", "InsectBuilder", + "builders": []string{"CallBuilder", "DeleteBuilder", "InsectBuilder", "InsertBuilder", "RawBuilder", "SelectBuilder", "SelectDocBuilder", "UpdateBuilder", "UpsertBuilder"}, } @@ -43,7 +43,7 @@ func generateTasks(p *do.Project) { s, err := util.StrTemplate(builderTemplate, context) c.Check(err, "Unalbe ") - ioutil.WriteFile("v1/builders_generated.go", []byte(s), 0644) - c.Run("go fmt v1/builders_generated.go") + ioutil.WriteFile("builders_generated.go", []byte(s), 0644) + c.Run("go fmt builders_generated.go") }).Desc("Generates builder boilerplate code") } diff --git a/Gododir/main.go b/Gododir/main.go index e3ec53e..f0e9f5c 100644 --- a/Gododir/main.go +++ b/Gododir/main.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + _ "github.com/lib/pq" do "gopkg.in/godo.v2" ) @@ -16,9 +18,16 @@ func tasks(p *do.Project) { p.Task("createdb", nil, createdb).Description("Creates test database") p.Task("test", nil, func(c *do.Context) { + c.Run(`go test -race`) + c.Run(`go test -race`, do.M{"$in": "sqlx-runner"}) + }).Src("**/*.go"). + Desc("test with -race flag") + + p.Task("test-fast", nil, func(c *do.Context) { c.Run(`go test`) c.Run(`go test`, do.M{"$in": "sqlx-runner"}) - }).Src("**/*.go") + }).Src("**/*.go"). + Desc("fater test without -race flag") p.Task("test-dir", nil, func(c *do.Context) { dir := c.Args.NonFlags()[0] @@ -36,6 +45,10 @@ func tasks(p *do.Project) { `) }) + p.Task("hello", nil, func(*do.Context) { + fmt.Println("hello?") + }) + p.Task("bench", nil, func(c *do.Context) { // Bash("go test -bench . -benchmem 2>/dev/null | column -t") // Bash("go test -bench . -benchmem 2>/dev/null | column -t", In{"sqlx-runner"}) @@ -62,6 +75,33 @@ func tasks(p *do.Project) { p.Task("example", nil, func(c *do.Context) { }) + + p.Task("lint", nil, func(c *do.Context) { + c.Bash(` + echo Directory=. + golint + + cd sqlx-runner + echo + echo Directory=sqlx-runner + golint + + cd ../kvs + echo + echo Directory=kvs + golint + + cd ../postgres + echo + echo Directory=postgres + golint + `) + }) + + p.Task("mocks", nil, func(c *do.Context) { + // go get github.com/vektra/mockery + c.Run("mockery --dir=kvs --all") + }) } func main() { diff --git a/README.md b/README.md index 6de2088..4bf477b 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ library for Go. DB.SQL(`SELECT * FROM people LIMIT 10`).QueryStructs(&people) ``` -* JSON Document retrieval (single trip to Postgres!, requires Postgres 9.3+) +* JSON Document retrieval (single trip to Postgres, requires Postgres 9.3+) ```go DB.SelectDoc("id", "user_name", "avatar"). @@ -140,7 +140,7 @@ func init() { panic("Could not ping database") } - // set to reasonable values + // set to reasonable values for production db.SetMaxIdleConns(4) db.SetMaxOpenConns(16) @@ -163,7 +163,7 @@ type Post struct { Body string `db:"body"` UserID int64 `db:"user_id"` State string `db:"state"` - UpdatedAt dat.Nulltime `db:"updated_at"` + UpdatedAt dat.NullTime `db:"updated_at"` CreatedAt dat.NullTime `db:"created_at"` } @@ -260,7 +260,6 @@ DB.InsertInto("payments"). // ensure session user can only update his information DB.Update("users"). SetWhitelist(user, "user_name", "avatar", "quote"). - Record(userData). Where("id = $1", session.UserID). Exec() ``` @@ -282,7 +281,7 @@ b.MustInterpolate() == "SELECT * FROM posts WHERE id IN (10,20,30,40,50)" `dat` uses [logxi](https://github.com/mgutz/logxi) for logging. By default, *logxi* logs all warnings and errors to the console. `dat` logs the SQL and its arguments on any error. In addition, `dat` logs slow queries -as warnings if `LogQueriesThreshold > 0` +as warnings if `runner.LogQueriesThreshold > 0` To trace all SQL, set environment variable @@ -298,7 +297,7 @@ Use `Returning` and `QueryStruct` to insert and update struct fields in one trip ```go -post := Post{Title: "Swith to Postgres", State: "open"} +var post Post err := DB. InsertInto("posts"). @@ -316,7 +315,7 @@ post := Post{Title: "Go is awesome", State: "open"} err := DB. InsertInto("posts"). Blacklist("id", "user_id", "created_at", "updated_at"). - Record(post). + Record(&post). Returning("id", "created_at", "updated_at"). QueryStruct(&post) @@ -324,7 +323,7 @@ err := DB. err := DB. InsertInto("posts"). Whitelist("*"). - Record(post). + Record(&post). Returning("id", "created_at", "updated_at"). QueryStruct(&post) @@ -491,7 +490,7 @@ result, err = DB. ### Joins -Define JOINs as arguments to `From` +Define JOINs in argument to `From` ``` go err = DB. @@ -594,6 +593,52 @@ func getUsers(conn runner.Connection) ([]*dto.Users, error) { } ``` +#### Nested Transactions + +Nested transaction logic is as follows: + +* If `Commit` is called in a nested transaction, the operation results in no operation (NOOP). + Only the top level `Commit` commits the transaction to the database. + +* If `Rollback` is called in a nested transaction, then the entire + transaction is rolled back. `Tx.IsRollbacked` is set to true. + +* Either `defer Tx.AutoCommit()` or `defer Tx.AutoRollback()` **MUST BE CALLED** + for each corresponding `Begin`. The internal state of nested transactions is + tracked in these two methods. + +```go +func nested(conn runner.Connection) error { + tx, err := conn.Begin() + if err != nil { + return err + } + defer tx.AutoRollback() + + _, err := tx.SQL(`INSERT INTO users (email) values $1`, 'me@home.com').Exec() + if err != nil { + return err + } + // prevents AutoRollback + tx.Commit() +} + +func top() { + tx, err := DB.Begin() + if err != nil { + logger.Fatal("Could not create transaction") + } + defer tx.AutoRollback() + + err := nested(tx) + if err != nil { + return + } + // top level commits the transaction + tx.Commit() +} +``` + ### Dates Use `dat.NullTime` type to properly handle nullable dates @@ -661,80 +706,6 @@ err := DB. QueryStruct(&post) ``` -### Transactions - -```go -// Start transaction -tx, err := DB.Begin() -if err != nil { - return err -} -// safe to call tx.Rollback() or tx.Commit() later -defer tx.AutoRollback() - -// Issue statements that might cause errors -res, err := tx. - Update("posts"). - Set("state", "deleted"). - Where("deleted_at IS NOT NULL"). - Exec() - -if err != nil { - // AutoRollback will rollback the transaction - return err -} - -// commit to prevent AutoRollback from rolling back the transaction -tx.Commit() -return nil -``` - -#### Nested Transactions - -Nested transaction logic is as follows: - -* If `Commit` is called in a nested transaction, the operation results in no operatoin (NOOP). - Only the top level `Commit` commits the transaction to the database. - -* If `Rollback` is called in a nested transaction, then the entire - transaction is rolled back. `Tx.IsRollbacked` is set to true. - -* Either `defer Tx.AutoCommit()` or `defer Tx.AutoRollback()` **MUST BE CALLED** - for each corresponding `Begin`. The internal state of nested transactions is - tracked in these two methods. - -```go -func nested(conn runner.Connection) error { - tx, err := conn.Begin() - if err != nil { - return err - } - defer tx.AutoRollback() - - _, err := tx.SQL(`INSERT INTO users (email) values $1`, 'me@home.com').Exec() - if err != nil { - return err - } - // prevents AutoRollback - tx.Commit() -} - -func top() { - tx, err := DB.Begin() - if err != nil { - logger.Fatal("Could not create transaction") - } - defer tx.AutoRollback() - - err := nested(tx) - if err != nil { - return - } - // top level commits the transaction - tx.Commit() -} -``` - ### Caching dat implements caching backed by an in-memory or Redis store. The in-memory store diff --git a/builder.go b/builder.go index 99a68ce..73ea428 100644 --- a/builder.go +++ b/builder.go @@ -1,15 +1,5 @@ package dat -import "time" - -// Cacher caches query results. -type Cacher interface { - // Cache caches the result of a Select or SelectDoc. If id is not provided, an FNV checksum - // of the SQL is used as the id. (If interpolation is set, arguments are hashed). Use invalidate to - // immediately invalidate the cache to force setting its value. - Cache(id string, duration time.Duration, invalidate bool) -} - // Builder interface is used to tie SQL generators to executors. type Builder interface { // ToSQL builds the SQL and arguments from builder. diff --git a/execer.go b/execer.go index e7861c0..5e92e28 100644 --- a/execer.go +++ b/execer.go @@ -27,7 +27,7 @@ var nullExecer = &panicExecer{} // panicExecer is the execer assigned when a builder is first created. // panicExecer raises a panic if any of the Execer methods are called -// directly from dat. Runners override the execer to communicate with a live +// directly from dat. Runners override the execer to work with a live // database. type panicExecer struct{} diff --git a/generate.go b/generate.go deleted file mode 100644 index 2671069..0000000 --- a/generate.go +++ /dev/null @@ -1,5 +0,0 @@ -package dat - -func Generate(srcDir string, destDir string) error { - return nil -} diff --git a/insert.go b/insert.go index 068cdd9..3b06fe2 100644 --- a/insert.go +++ b/insert.go @@ -43,7 +43,7 @@ func (b *InsertBuilder) Blacklist(columns ...string) *InsertBuilder { } // Whitelist defines a whitelist of columns to be inserted. To -// specify all columsn of a record use "*". +// specify all columns of a record use "*". func (b *InsertBuilder) Whitelist(columns ...string) *InsertBuilder { b.cols = columns return b diff --git a/interpolate.go b/interpolate.go index 123f87e..0a0467a 100644 --- a/interpolate.go +++ b/interpolate.go @@ -63,7 +63,7 @@ func Interpolate(sql string, vals []interface{}) (string, []interface{}, error) // Args with a blank query is an error if sql == "" { if lenVals != 0 { - return "", nil, ErrArgumentMismatch + return "", nil, logger.Error("Interpolation error", "err", ErrArgumentMismatch, "sql", sql, "args", vals) } return "", nil, nil } @@ -75,13 +75,13 @@ func Interpolate(sql string, vals []interface{}) (string, []interface{}, error) // No args for a query with place holders is an error if lenVals == 0 { if hasPlaceholders { - return "", nil, ErrArgumentMismatch + return "", nil, logger.Error("Interpolation error", "err", ErrArgumentMismatch, "sql", sql, "args", vals) } return sql, nil, nil } if lenVals > 0 && !hasPlaceholders { - return "", nil, ErrArgumentMismatch + return "", nil, logger.Error("Interpolation error", "err", ErrArgumentMismatch, "sql", sql, "args", vals) } if !hasPlaceholders { diff --git a/interpolate_test.go b/interpolate_test.go index 691ad8c..122bf60 100644 --- a/interpolate_test.go +++ b/interpolate_test.go @@ -61,9 +61,9 @@ func TestInterpolateInts(t *testing.T) { uint64(10), } - str, _, err := Interpolate("SELECT * FROM x WHERE a = $1 AND b = $2 AND c = $3 AND d = $4 AND e = $5 AND f = $6 AND g = $7 AND h = $8 AND i = $9 AND j = $10", args) + str, _, err := Interpolate("SELECT * FROM x WHERE a = $1 AND b = $2 AND c = $3 AND d = $4 AND e = $5 AND f = $6 AND g = $7 AND h = $8 AND i = $9 AND j = $1", args) assert.NoError(t, err) - assert.Equal(t, str, "SELECT * FROM x WHERE a = 1 AND b = -2 AND c = 3 AND d = 4 AND e = 5 AND f = 6 AND g = 7 AND h = 8 AND i = 9 AND j = 10") + assert.Equal(t, str, "SELECT * FROM x WHERE a = 1 AND b = -2 AND c = 3 AND d = 4 AND e = 5 AND f = 6 AND g = 7 AND h = 8 AND i = 9 AND j = 1") } func TestInterpolateBools(t *testing.T) { diff --git a/kvs/interfaces.go b/kvs/interfaces.go index edae2af..b2d485b 100644 --- a/kvs/interfaces.go +++ b/kvs/interfaces.go @@ -15,7 +15,7 @@ type KeyValueStore interface { FlushDB() error } -// TTLNever means do not attach a TTL to a key +// TTLNever means do not expire a key const TTLNever time.Duration = -1 // NanosecondsPerMillisecond is used to convert between ns and ms. diff --git a/postgres/postgres.go b/postgres/postgres.go index 42832a1..f5b7ede 100644 --- a/postgres/postgres.go +++ b/postgres/postgres.go @@ -42,7 +42,8 @@ func New() *Postgres { return &Postgres{} } -// WriteStringLiteral is part of Dialect implementation. +// WriteStringLiteral writes an escaped string. No escape characters +// are allowed. // // Postgres 9.1+ does not allow any escape // sequences by default. See http://www.postgresql.org/docs/9.3/interactive/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE @@ -89,7 +90,7 @@ func (pd *Postgres) WriteStringLiteral(buf common.BufferWriter, val string) { } } -// WriteIdentifier is part of Dialect implementation. +// WriteIdentifier writes escaped identifier. func (pd *Postgres) WriteIdentifier(buf common.BufferWriter, ident string) { if ident == "" { panic("Identifier is empty string") diff --git a/select_doc_test.go b/select_doc_test.go index 9bd0081..7424d50 100644 --- a/select_doc_test.go +++ b/select_doc_test.go @@ -2,6 +2,7 @@ package dat import ( "testing" + "time" "gopkg.in/stretchr/testify.v1/assert" ) @@ -72,56 +73,33 @@ func TestSelectDocSQLInnerSQL(t *testing.T) { assert.Equal(t, []interface{}{4, 4}, args) } -// func TestSelectDocSQLReturning(t *testing.T) { -// sql, args := SelectDoc("tab").Columns("b", "c").Values(1, 2).Where("d=$1", 4).Returning("f", "g").ToSQL() -// expected := ` -// WITH -// upd AS ( -// UPDATE tab -// SET "b" = $1, "c" = $2 -// WHERE (d=$3) -// RETURNING "f","g" -// ), ins AS ( -// INSERT INTO "tab"("b","c") -// SELECT $1,$2 -// WHERE NOT EXISTS (SELECT 1 FROM upd) -// RETURNING "f","g" -// ) -// SELECT * FROM ins UNION ALL SELECT * FROM upd -// ` +func TestSelectDocScope(t *testing.T) { + now := NullTimeFrom(time.Now()) -// assert.Equal(t, stripWS(expected), stripWS(sql)) -// assert.Equal(t, []interface{}{1, 2, 4}, args) -// } - -// func TestSelectDocSQLRecord(t *testing.T) { -// var rec = struct { -// B int -// C int -// }{1, 2} -// sql, args := SelectDoc("tab"). -// Columns("b", "c"). -// Record(rec). -// Where("d=$1", 4). -// Returning("f", "g"). -// ToSQL() + sql, args := SelectDoc("e", "f"). + From("matches m"). + Scope(` + WHERE m.game_id = $1 + AND ( + m.id > $3 + OR (m.id >= $2 AND m.id <= $3 AND m.updated_at > $4) + ) + `, 100, 1, 2, now). + ToSQL() -// expected := ` -// WITH -// upd AS ( -// UPDATE tab -// SET "b" = $1, "c" = $2 -// WHERE (d=$3) -// RETURNING "f","g" -// ), ins AS ( -// INSERT INTO "tab"("b","c") -// SELECT $1,$2 -// WHERE NOT EXISTS (SELECT 1 FROM upd) -// RETURNING "f","g" -// ) -// SELECT * FROM ins UNION ALL SELECT * FROM upd -// ` + expected := ` + SELECT row_to_json(dat__item.*) + FROM ( + SELECT e, f + FROM matches m + WHERE m.game_id=$1 + AND ( + m.id > $3 + OR (m.id >= $2 AND m.id<=$3 AND m.updated_at>$4) + ) + ) as dat__item + ` -// assert.Equal(t, stripWS(expected), stripWS(sql)) -// assert.Equal(t, []interface{}{1, 2, 4}, args) -// } + assert.Equal(t, stripWS(expected), stripWS(sql)) + assert.Equal(t, []interface{}{100, 1, 2, now}, args) +} diff --git a/sqlx-runner/db.go b/sqlx-runner/db.go index 3e2d688..37b7e8d 100644 --- a/sqlx-runner/db.go +++ b/sqlx-runner/db.go @@ -51,7 +51,6 @@ func NewDB(db *sql.DB, driverName string) *DB { if dat.Strict { conn.SQL("SET client_min_messages to 'DEBUG';") } - } else { panic("Unsupported driver: " + driverName) } diff --git a/sqlx-runner/exec.go b/sqlx-runner/exec.go index 007aa87..88fa3a3 100644 --- a/sqlx-runner/exec.go +++ b/sqlx-runner/exec.go @@ -154,13 +154,7 @@ func queryScalar(execer *Execer, destinations ...interface{}) error { return logSQLError(err, "QueryScalar.load_value.scan", fullSQL, args) } - if execer.cacheTTL > 0 { - blob, err = json.Marshal(destinations) - if err != nil { - logger.Warn("queryScalar.4: Could not marshal cache data") - } - setCache(execer, blob) - } + setCache(execer, destinations, dtStruct) return nil } @@ -246,13 +240,7 @@ func querySlice(execer *Execer, dest interface{}) error { return logSQLError(err, "querySlice.load_all_values.rows_err", fullSQL, args) } - if execer.cacheTTL > 0 { - blob, err = json.Marshal(dest) - if err != nil { - logger.Warn("queryStruct.4: Could not marshal cache data") - } - setCache(execer, blob) - } + setCache(execer, dest, dtStruct) return nil } @@ -286,13 +274,8 @@ func queryStruct(execer *Execer, dest interface{}) error { return err } - if execer.cacheTTL > 0 { - blob, err = json.Marshal(dest) - if err != nil { - logger.Warn("queryStruct.4: Could not marshal cache data") - } - setCache(execer, blob) - } + setCache(execer, dest, dtStruct) + return nil } @@ -326,13 +309,7 @@ func queryStructs(execer *Execer, dest interface{}) error { logSQLError(err, "queryStructs", fullSQL, args) } - if execer.cacheTTL > 0 { - blob, err = json.Marshal(dest) - if err != nil { - logger.Warn("queryStruct.4: Could not marshal cache data") - } - setCache(execer, blob) - } + setCache(execer, dest, dtStruct) return err } @@ -421,8 +398,7 @@ func queryJSONBlob(execer *Execer, single bool) ([]byte, error) { } blob = buf.Bytes() - setCache(execer, blob) - + setCache(execer, blob, dtBytes) return blob, nil } @@ -442,10 +418,12 @@ func queryJSONStructs(execer *Execer, dest interface{}) error { // the SQL and args to be executed. If value = "" then the SQL is built. func cacheOrSQL(execer *Execer) (sql string, args []interface{}, value []byte, err error) { // if a cacheID exists, return the value ASAP - if Cache != nil && !execer.cacheInvalidate && execer.cacheTTL > 0 && execer.cacheID != "" { + if Cache != nil && execer.cacheTTL > 0 && execer.cacheID != "" && !execer.cacheInvalidate { v, err := Cache.Get(execer.cacheID) //logger.Warn("DBG cacheOrSQL.1 getting by id", "id", execer.cacheID, "v", v, "err", err) - if v != "" && (err == nil || err != kvs.ErrNotFound) { + if err != nil && err != kvs.ErrNotFound { + logger.Error("Unable to read cache key. Continuing with query", "key", execer.cacheID) + } else if v != "" { //logger.Warn("DBG cacheOrSQL.11 HIT", "v", v) return "", nil, []byte(v), nil } @@ -456,7 +434,7 @@ func cacheOrSQL(execer *Execer) (sql string, args []interface{}, value []byte, e return "", nil, nil, err } - // since there is no cacheID, use the SQL as the ID + // if there is no cacheID, use the checksum of SQL as the ID if Cache != nil && execer.cacheTTL > 0 && execer.cacheID == "" { // this must be set for setCache() to work below execer.cacheID = kvs.Hash(fullSQL) @@ -474,16 +452,44 @@ func cacheOrSQL(execer *Execer) (sql string, args []interface{}, value []byte, e return fullSQL, args, nil, nil } +const ( + dtStruct = iota + dtString + dtBytes +) + // Sets the cache value using the execer.ID key. Note that execer.ID // is set as a side-effect of calling cacheOrSQL function above if -// execer.cacheID is not set. -func setCache(execer *Execer, b []byte) { - if Cache != nil && execer.cacheTTL > 0 { - //logger.Warn("DBG setting cache", "key", execer.cacheID, "data", string(b), "ttl", execer.cacheTTL) - err := Cache.Set(execer.cacheID, string(b), execer.cacheTTL) +// execer.cacheID is not set. data must be a string or a value that +// can be json.Marshal'ed to string. +func setCache(execer *Execer, data interface{}, dataType int) { + if Cache == nil || execer.cacheTTL < 1 { + return + } + + var s string + switch dataType { + case dtStruct: + b, err := json.Marshal(data) if err != nil { - logger.Warn("Could not set cache. Query will proceed without caching", "err", err) + logger.Warn("Could not marshal data, clearing", "key", execer.cacheID) + err = Cache.Del(execer.cacheID) + if err != nil { + logger.Error("Could not delete cache key", "key", execer.cacheID) + } + return } + s = string(b) + case dtString: + s = data.(string) + case dtBytes: + s = string(data.([]byte)) + } + + //logger.Warn("DBG setting cache", "key", execer.cacheID, "data", string(b), "ttl", execer.cacheTTL) + err := Cache.Set(execer.cacheID, s, execer.cacheTTL) + if err != nil { + logger.Warn("Could not set cache. Query will proceed without caching", "err", err) } } @@ -512,7 +518,7 @@ func queryJSON(execer *Execer) ([]byte, error) { logSQLError(err, "queryJSON", jsonSQL, args) } - setCache(execer, blob) + setCache(execer, blob, dtBytes) return blob, err } diff --git a/types.go b/types.go index 367f2d5..898442b 100644 --- a/types.go +++ b/types.go @@ -60,27 +60,27 @@ type NullBool struct { // NullStringFrom creates a valid NullString func NullStringFrom(v string) NullString { - return NullString{sql.NullString{v, true}} + return NullString{sql.NullString{String: v, Valid: true}} } // NullFloat64From creates a valid NullFloat64 func NullFloat64From(v float64) NullFloat64 { - return NullFloat64{sql.NullFloat64{v, true}} + return NullFloat64{sql.NullFloat64{Float64: v, Valid: true}} } // NullInt64From creates a valid NullInt64 func NullInt64From(v int64) NullInt64 { - return NullInt64{sql.NullInt64{v, true}} + return NullInt64{sql.NullInt64{Int64: v, Valid: true}} } // NullTimeFrom creates a valid NullTime func NullTimeFrom(v time.Time) NullTime { - return NullTime{pq.NullTime{v, true}} + return NullTime{pq.NullTime{Time: v, Valid: true}} } // NullBoolFrom creates a valid NullBool func NullBoolFrom(v bool) NullBool { - return NullBool{sql.NullBool{v, true}} + return NullBool{sql.NullBool{Bool: v, Valid: true}} } var nullString = []byte("null") diff --git a/where.go b/where.go index 6fb6f5d..eef8f61 100644 --- a/where.go +++ b/where.go @@ -2,6 +2,8 @@ package dat import ( "reflect" + "regexp" + "strconv" "strings" "gopkg.in/mgutz/dat.v1/common" @@ -33,31 +35,28 @@ func newWhereFragment(whereSqlOrMap interface{}, args []interface{}) *whereFragm } } -func remapPlaceholders(buf common.BufferWriter, statement string, pos int64) int64 { +var rePlaceholder = regexp.MustCompile(`\$\d+`) + +func remapPlaceholders(buf common.BufferWriter, statement string, start int64) int64 { if !strings.Contains(statement, "$") { buf.WriteString(statement) return 0 } - var discardDigits bool - var replaced int64 - for _, r := range statement { - if discardDigits { - if '0' <= r && r <= '9' { - continue - } - discardDigits = false - } - if r != '$' { - buf.WriteRune(r) - } else if r == '$' { - // replace relative $1 with absolute like $4 - writePlaceholder64(buf, pos+replaced) - replaced++ - discardDigits = true + highest := 0 + pos := int(start) - 1 // 0-based + statement = rePlaceholder.ReplaceAllStringFunc(statement, func(s string) string { + i, _ := strconv.Atoi(s[1:]) + if i > highest { + highest = i } - } - return replaced + + sum := strconv.Itoa(pos + i) + return "$" + sum + }) + + buf.WriteString(statement) + return int64(highest) } // Invariant: for scope conditions only