Skip to content

Commit

Permalink
Partial revert of b842b69
Browse files Browse the repository at this point in the history
Restore dialect_postgres.go and dialect_postgres_test.go

Restore relevant README sections and interface test.

Restore GitHub Workflow configuration.
  • Loading branch information
jsha committed Mar 27, 2024
1 parent 52168b2 commit 980e50d
Show file tree
Hide file tree
Showing 5 changed files with 353 additions and 2 deletions.
14 changes: 13 additions & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,19 @@ jobs:
- "1.21-rc"
runs-on: ubuntu-latest
container: golang:${{ matrix.containerGoVer }}
services:
services:
postgres:
image: postgres
env:
POSTGRES_DB: gorptest
POSTGRES_USER: gorptest
POSTGRES_PASSWORD: gorptest
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 10
mysql:
image: mariadb:10.5
env:
Expand Down
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ func main() {

// fetch one row - note use of "post_id" instead of "Id" since column is aliased
//
// Postgres users should use $1 instead of ? placeholders
// See 'Known Issues' below

err = dbmap.SelectOne(&p2, "select * from posts where post_id=?", p2.Id)
checkErr(err, "SelectOne failed")
log.Println("p2 row:", p2)
Expand Down Expand Up @@ -368,7 +371,7 @@ if reflect.DeepEqual(list[0], expected) {
Borp provides a few convenience methods for selecting a single string or int64.

```go
// select single int64 from db
// select single int64 from db (use $1 instead of ? for postgresql)
i64, err := dbmap.SelectInt("select count(*) from foo where blah=?", blahVal)

// select single string from db:
Expand Down Expand Up @@ -579,6 +582,7 @@ interface that should be implemented per database vendor. Dialects
are provided for:

* MySQL
* PostgreSQL
* sqlite3

Each of these three databases pass the test suite. See `borp_test.go`
Expand Down Expand Up @@ -612,6 +616,41 @@ func customDriver() (*sql.DB, error) {

## Known Issues

### SQL placeholder portability

Different databases use different strings to indicate variable
placeholders in prepared SQL statements. Unlike some database
abstraction layers (such as JDBC), Go's `database/sql` does not
standardize this.

SQL generated by gorp in the `Insert`, `Update`, `Delete`, and `Get`
methods delegates to a Dialect implementation for each database, and
will generate portable SQL.

Raw SQL strings passed to `Exec`, `Select`, `SelectOne`, `SelectInt`,
etc will not be parsed. Consequently you may have portability issues
if you write a query like this:

```go
// works on MySQL and Sqlite3, but not with Postgresql err :=
dbmap.SelectOne(&val, "select * from foo where id = ?", 30)
```

In `Select` and `SelectOne` you can use named parameters to work
around this. The following is portable:

```go
err := dbmap.SelectOne(&val, "select * from foo where id = :id",
map[string]interface{} { "id": 30})
```

Additionally, when using Postgres as your database, you should utilize
`$1` instead of `?` placeholders as utilizing `?` placeholders when
querying Postgres will result in `pq: operator does not exist`
errors. Alternatively, use `dbMap.Dialect.BindVar(varIdx)` to get the
proper variable binding for your dialect.


### time.Time and time zones

borp will pass `time.Time` fields through to the `database/sql`
Expand Down
149 changes: 149 additions & 0 deletions dialect_postgres.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright 2012 James Cooper. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package gorp

import (
"fmt"
"reflect"
"strings"
"time"
)

type PostgresDialect struct {
suffix string
LowercaseFields bool
}

func (d PostgresDialect) QuerySuffix() string { return ";" }

func (d PostgresDialect) ToSqlType(val reflect.Type, maxsize int, isAutoIncr bool) string {
switch val.Kind() {
case reflect.Ptr:
return d.ToSqlType(val.Elem(), maxsize, isAutoIncr)
case reflect.Bool:
return "boolean"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
if isAutoIncr {
return "serial"
}
return "integer"
case reflect.Int64, reflect.Uint64:
if isAutoIncr {
return "bigserial"
}
return "bigint"
case reflect.Float64:
return "double precision"
case reflect.Float32:
return "real"
case reflect.Slice:
if val.Elem().Kind() == reflect.Uint8 {
return "bytea"
}
}

switch val.Name() {
case "NullInt64":
return "bigint"
case "NullFloat64":
return "double precision"
case "NullBool":
return "boolean"
case "Time", "NullTime":
return "timestamp with time zone"
}

if maxsize > 0 {
return fmt.Sprintf("varchar(%d)", maxsize)
} else {
return "text"
}

}

// Returns empty string
func (d PostgresDialect) AutoIncrStr() string {
return ""
}

func (d PostgresDialect) AutoIncrBindValue() string {
return "default"
}

func (d PostgresDialect) AutoIncrInsertSuffix(col *ColumnMap) string {
return " returning " + d.QuoteField(col.ColumnName)
}

// Returns suffix
func (d PostgresDialect) CreateTableSuffix() string {
return d.suffix
}

func (d PostgresDialect) CreateIndexSuffix() string {
return "using"
}

func (d PostgresDialect) DropIndexSuffix() string {
return ""
}

func (d PostgresDialect) TruncateClause() string {
return "truncate"
}

func (d PostgresDialect) SleepClause(s time.Duration) string {
return fmt.Sprintf("pg_sleep(%f)", s.Seconds())
}

// Returns "$(i+1)"
func (d PostgresDialect) BindVar(i int) string {
return fmt.Sprintf("$%d", i+1)
}

func (d PostgresDialect) InsertAutoIncrToTarget(exec SqlExecutor, insertSql string, target interface{}, params ...interface{}) error {
rows, err := exec.Query(insertSql, params...)
if err != nil {
return err
}
defer rows.Close()

if !rows.Next() {
return fmt.Errorf("No serial value returned for insert: %s Encountered error: %s", insertSql, rows.Err())
}
if err := rows.Scan(target); err != nil {
return err
}
if rows.Next() {
return fmt.Errorf("more than two serial value returned for insert: %s", insertSql)
}
return rows.Err()
}

func (d PostgresDialect) QuoteField(f string) string {
if d.LowercaseFields {
return `"` + strings.ToLower(f) + `"`
}
return `"` + f + `"`
}

func (d PostgresDialect) QuotedTableForQuery(schema string, table string) string {
if strings.TrimSpace(schema) == "" {
return d.QuoteField(table)
}

return schema + "." + d.QuoteField(table)
}

func (d PostgresDialect) IfSchemaNotExists(command, schema string) string {
return fmt.Sprintf("%s if not exists", command)
}

func (d PostgresDialect) IfTableExists(command, schema, table string) string {
return fmt.Sprintf("%s if exists", command)
}

func (d PostgresDialect) IfTableNotExists(command, schema, table string) string {
return fmt.Sprintf("%s if not exists", command)
}
150 changes: 150 additions & 0 deletions dialect_postgres_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright 2012 James Cooper. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

// +build !integration

package gorp_test

import (
"database/sql"
"reflect"
"testing"
"time"

"github.com/go-gorp/gorp/v3"
"github.com/poy/onpar"
"github.com/poy/onpar/expect"
"github.com/poy/onpar/matchers"
)

func TestPostgresDialect(t *testing.T) {
o := onpar.New()
defer o.Run(t)

o.BeforeEach(func(t *testing.T) (expect.Expectation, gorp.PostgresDialect) {
return expect.New(t), gorp.PostgresDialect{
LowercaseFields: false,
}
})

o.Group("ToSqlType", func() {
tests := []struct {
name string
value interface{}
maxSize int
autoIncr bool
expected string
}{
{"bool", true, 0, false, "boolean"},
{"int8", int8(1), 0, false, "integer"},
{"uint8", uint8(1), 0, false, "integer"},
{"int16", int16(1), 0, false, "integer"},
{"uint16", uint16(1), 0, false, "integer"},
{"int32", int32(1), 0, false, "integer"},
{"int (treated as int32)", int(1), 0, false, "integer"},
{"uint32", uint32(1), 0, false, "integer"},
{"uint (treated as uint32)", uint(1), 0, false, "integer"},
{"int64", int64(1), 0, false, "bigint"},
{"uint64", uint64(1), 0, false, "bigint"},
{"float32", float32(1), 0, false, "real"},
{"float64", float64(1), 0, false, "double precision"},
{"[]uint8", []uint8{1}, 0, false, "bytea"},
{"NullInt64", sql.NullInt64{}, 0, false, "bigint"},
{"NullFloat64", sql.NullFloat64{}, 0, false, "double precision"},
{"NullBool", sql.NullBool{}, 0, false, "boolean"},
{"Time", time.Time{}, 0, false, "timestamp with time zone"},
{"default-size string", "", 0, false, "text"},
{"sized string", "", 50, false, "varchar(50)"},
{"large string", "", 1024, false, "varchar(1024)"},
}
for _, t := range tests {
o.Spec(t.name, func(expect expect.Expectation, dialect gorp.PostgresDialect) {
typ := reflect.TypeOf(t.value)
sqlType := dialect.ToSqlType(typ, t.maxSize, t.autoIncr)
expect(sqlType).To(matchers.Equal(t.expected))
})
}
})

o.Spec("AutoIncrStr", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
expect(dialect.AutoIncrStr()).To(matchers.Equal(""))
})

o.Spec("AutoIncrBindValue", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
expect(dialect.AutoIncrBindValue()).To(matchers.Equal("default"))
})

o.Spec("AutoIncrInsertSuffix", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
cm := gorp.ColumnMap{
ColumnName: "foo",
}
expect(dialect.AutoIncrInsertSuffix(&cm)).To(matchers.Equal(` returning "foo"`))
})

o.Spec("CreateTableSuffix", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
expect(dialect.CreateTableSuffix()).To(matchers.Equal(""))
})

o.Spec("CreateIndexSuffix", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
expect(dialect.CreateIndexSuffix()).To(matchers.Equal("using"))
})

o.Spec("DropIndexSuffix", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
expect(dialect.DropIndexSuffix()).To(matchers.Equal(""))
})

o.Spec("TruncateClause", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
expect(dialect.TruncateClause()).To(matchers.Equal("truncate"))
})

o.Spec("SleepClause", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
expect(dialect.SleepClause(1 * time.Second)).To(matchers.Equal("pg_sleep(1.000000)"))
expect(dialect.SleepClause(100 * time.Millisecond)).To(matchers.Equal("pg_sleep(0.100000)"))
})

o.Spec("BindVar", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
expect(dialect.BindVar(0)).To(matchers.Equal("$1"))
expect(dialect.BindVar(4)).To(matchers.Equal("$5"))
})

o.Group("QuoteField", func() {
o.Spec("By default, case is preserved", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
expect(dialect.QuoteField("Foo")).To(matchers.Equal(`"Foo"`))
expect(dialect.QuoteField("bar")).To(matchers.Equal(`"bar"`))
})

o.Group("With LowercaseFields set to true", func() {
o.BeforeEach(func(expect expect.Expectation, dialect gorp.PostgresDialect) (expect.Expectation, gorp.PostgresDialect) {
dialect.LowercaseFields = true
return expect, dialect
})

o.Spec("fields are lowercased", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
expect(dialect.QuoteField("Foo")).To(matchers.Equal(`"foo"`))
})
})
})

o.Group("QuotedTableForQuery", func() {
o.Spec("using the default schema", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
expect(dialect.QuotedTableForQuery("", "foo")).To(matchers.Equal(`"foo"`))
})

o.Spec("with a supplied schema", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
expect(dialect.QuotedTableForQuery("foo", "bar")).To(matchers.Equal(`foo."bar"`))
})
})

o.Spec("IfSchemaNotExists", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
expect(dialect.IfSchemaNotExists("foo", "bar")).To(matchers.Equal("foo if not exists"))
})

o.Spec("IfTableExists", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
expect(dialect.IfTableExists("foo", "bar", "baz")).To(matchers.Equal("foo if exists"))
})

o.Spec("IfTableNotExists", func(expect expect.Expectation, dialect gorp.PostgresDialect) {
expect(dialect.IfTableNotExists("foo", "bar", "baz")).To(matchers.Equal("foo if not exists"))
})
}
1 change: 1 addition & 0 deletions gorp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var (
// verify interface compliance
_ = []borp.Dialect{
borp.SqliteDialect{},
borp.PostgresDialect{},
borp.MySQLDialect{},
}

Expand Down

0 comments on commit 980e50d

Please sign in to comment.