├── bin ├── hermit.hcl ├── go ├── gofmt ├── .go@latest.pkg ├── .golangci-lint-1.41.1.pkg ├── golangci-lint ├── README.hermit.md ├── activate-hermit └── hermit ├── doc.go ├── go.mod ├── .github └── workflows │ └── ci.yml ├── go.sum ├── .golangci.yml ├── camelcase.go ├── integration_test.go ├── dialect_test.go ├── README.md ├── db_test.go ├── db.go └── dialect.go /bin/hermit.hcl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bin/go: -------------------------------------------------------------------------------- 1 | .go@latest.pkg -------------------------------------------------------------------------------- /bin/gofmt: -------------------------------------------------------------------------------- 1 | .go@latest.pkg -------------------------------------------------------------------------------- /bin/.go@latest.pkg: -------------------------------------------------------------------------------- 1 | hermit -------------------------------------------------------------------------------- /bin/.golangci-lint-1.41.1.pkg: -------------------------------------------------------------------------------- 1 | hermit -------------------------------------------------------------------------------- /bin/golangci-lint: -------------------------------------------------------------------------------- 1 | .golangci-lint-1.41.1.pkg -------------------------------------------------------------------------------- /bin/README.hermit.md: -------------------------------------------------------------------------------- 1 | # Hermit environment 2 | 3 | This is a [Hermit](https://github.com/cashapp/hermit) bin directory. 4 | 5 | The symlinks in this directory are managed by Hermit and will automatically 6 | download and install Hermit itself as well as packages. These packages are 7 | local to this environment. 8 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package sequel is a Go <-> SQL mapping package. 2 | // 3 | // Sequel is similar to SQLx, but with the goal of automating even more of the common 4 | // operations around Go <-> SQL interaction. 5 | // 6 | // See the [README.md](https://github.com/alecthomas/sequel) for more details. 7 | package sequel 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alecthomas/sequel 2 | 3 | require ( 4 | github.com/davecgh/go-spew v1.1.1 // indirect 5 | github.com/go-sql-driver/mysql v1.4.1 6 | github.com/lib/pq v1.2.0 7 | github.com/mattn/go-sqlite3 v1.9.0 8 | github.com/pkg/errors v0.8.1 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | github.com/stretchr/testify v1.2.2 11 | ) 12 | 13 | go 1.13 14 | -------------------------------------------------------------------------------- /bin/activate-hermit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file must be used with "source bin/activate-hermit" from bash or zsh. 3 | # You cannot run it directly 4 | 5 | if [ "${BASH_SOURCE-}" = "$0" ]; then 6 | echo "You must source this script: \$ source $0" >&2 7 | exit 33 8 | fi 9 | 10 | BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")" 11 | if "${BIN_DIR}/hermit" noop > /dev/null; then 12 | eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")" 13 | 14 | if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then 15 | hash -r 2>/dev/null 16 | fi 17 | 18 | echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated" 19 | fi 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | name: CI 7 | jobs: 8 | test: 9 | name: Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | - name: Init Hermit 15 | run: ./bin/hermit env -r >> $GITHUB_ENV 16 | - name: Test 17 | run: go test ./... 18 | lint: 19 | name: Lint 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v2 24 | - name: Init Hermit 25 | run: ./bin/hermit env -r >> $GITHUB_ENV 26 | - name: golangci-lint 27 | run: golangci-lint run 28 | -------------------------------------------------------------------------------- /bin/hermit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | if [ -z "${HERMIT_STATE_DIR}" ]; then 6 | case "$(uname -s)" in 7 | Darwin) 8 | export HERMIT_STATE_DIR="${HOME}/Library/Caches/hermit" 9 | ;; 10 | Linux) 11 | export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/hermit" 12 | ;; 13 | esac 14 | fi 15 | 16 | export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}" 17 | HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")" 18 | export HERMIT_CHANNEL 19 | export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit} 20 | 21 | if [ ! -x "${HERMIT_EXE}" ]; then 22 | echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2 23 | curl -fsSL "${HERMIT_DIST_URL}/install.sh" | /bin/bash 1>&2 24 | fi 25 | 26 | exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@" 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= 4 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 5 | github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= 6 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 7 | github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= 8 | github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 9 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 10 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 14 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 15 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: true 3 | 4 | output: 5 | print-issued-lines: false 6 | 7 | linters: 8 | enable-all: true 9 | disable: 10 | - interfacer 11 | - golint 12 | - scopelint 13 | - maligned 14 | - lll 15 | - gochecknoglobals 16 | - godox 17 | - funlen 18 | - wsl 19 | - exhaustive 20 | - exhaustivestruct 21 | - nlreturn 22 | - nolintlint 23 | - goerr113 24 | - paralleltest 25 | - gci 26 | - gofumpt 27 | - cyclop 28 | - wrapcheck 29 | - nilerr 30 | - sqlclosecheck 31 | - testpackage 32 | 33 | linters-settings: 34 | govet: 35 | check-shadowing: true 36 | gocyclo: 37 | min-complexity: 10 38 | dupl: 39 | threshold: 100 40 | goconst: 41 | min-len: 5 42 | min-occurrences: 3 43 | gocyclo: 44 | min-complexity: 20 45 | 46 | issues: 47 | max-issues-per-linter: 0 48 | max-same-issues: 0 49 | exclude-use-default: false 50 | exclude: 51 | - '^(G104|G204):' 52 | # Very commonly not checked. 53 | - 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked' 54 | - 'exported method (.*\.MarshalJSON|.*\.UnmarshalJSON) should have comment or be unexported' 55 | - 'composite literal uses unkeyed fields' 56 | - 'bad syntax for struct tag key' 57 | - 'bad syntax for struct tag pair' 58 | -------------------------------------------------------------------------------- /camelcase.go: -------------------------------------------------------------------------------- 1 | package sequel 2 | 3 | // NOTE: This code is from https://github.com/fatih/camelcase. MIT license. 4 | 5 | import ( 6 | "unicode" 7 | "unicode/utf8" 8 | ) 9 | 10 | // Split splits the camelcase word and returns a list of words. It also 11 | // supports digits. Both lower camel case and upper camel case are supported. 12 | // For more info please check: http://en.wikipedia.org/wiki/CamelCase 13 | // 14 | // Examples 15 | // 16 | // "" => [""] 17 | // "lowercase" => ["lowercase"] 18 | // "Class" => ["Class"] 19 | // "MyClass" => ["My", "Class"] 20 | // "MyC" => ["My", "C"] 21 | // "HTML" => ["HTML"] 22 | // "PDFLoader" => ["PDF", "Loader"] 23 | // "AString" => ["A", "String"] 24 | // "SimpleXMLParser" => ["Simple", "XML", "Parser"] 25 | // "vimRPCPlugin" => ["vim", "RPC", "Plugin"] 26 | // "GL11Version" => ["GL", "11", "Version"] 27 | // "99Bottles" => ["99", "Bottles"] 28 | // "May5" => ["May", "5"] 29 | // "BFG9000" => ["BFG", "9000"] 30 | // "BöseÜberraschung" => ["Böse", "Überraschung"] 31 | // "Two spaces" => ["Two", " ", "spaces"] 32 | // "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"] 33 | // 34 | // Splitting rules 35 | // 36 | // 1) If string is not valid UTF-8, return it without splitting as 37 | // single item array. 38 | // 2) Assign all unicode characters into one of 4 sets: lower case 39 | // letters, upper case letters, numbers, and all other characters. 40 | // 3) Iterate through characters of string, introducing splits 41 | // between adjacent characters that belong to different sets. 42 | // 4) Iterate through array of split strings, and if a given string 43 | // is upper case: 44 | // if subsequent string is lower case: 45 | // move last character of upper case string to beginning of 46 | // lower case string 47 | func camelCase(src string) (entries []string) { 48 | // don't split invalid utf8 49 | if !utf8.ValidString(src) { 50 | return []string{src} 51 | } 52 | entries = []string{} 53 | var runes [][]rune 54 | lastClass := 0 55 | // split into fields based on class of unicode character 56 | for _, r := range src { 57 | var class int 58 | switch { 59 | case unicode.IsLower(r): 60 | class = 1 61 | case unicode.IsUpper(r): 62 | class = 2 63 | case unicode.IsDigit(r): 64 | class = 3 65 | default: 66 | class = 4 67 | } 68 | if class == lastClass { 69 | runes[len(runes)-1] = append(runes[len(runes)-1], r) 70 | } else { 71 | runes = append(runes, []rune{r}) 72 | } 73 | lastClass = class 74 | } 75 | // handle upper case -> lower case sequences, e.g. 76 | // "PDFL", "oader" -> "PDF", "Loader" 77 | for i := 0; i < len(runes)-1; i++ { 78 | if unicode.IsUpper(runes[i][0]) && unicode.IsLower(runes[i+1][0]) { 79 | runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...) 80 | runes[i] = runes[i][:len(runes[i])-1] 81 | } 82 | } 83 | // construct []string from results 84 | for _, s := range runes { 85 | if len(s) > 0 { 86 | entries = append(entries, string(s)) 87 | } 88 | } 89 | return entries 90 | } 91 | -------------------------------------------------------------------------------- /integration_test.go: -------------------------------------------------------------------------------- 1 | // +build integration 2 | 3 | package sequel 4 | 5 | import ( 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | _ "github.com/go-sql-driver/mysql" // imported for side-effects 11 | _ "github.com/lib/pq" // imported for side-effects 12 | _ "github.com/mattn/go-sqlite3" // imported for side-effects 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | const sqliteTestFile = "./sqlite_integration_test.db" 17 | 18 | func TestDialects(t *testing.T) { 19 | type User struct { 20 | ID int `db:",pk,managed"` 21 | Created time.Time `db:",managed"` 22 | Name string 23 | } 24 | 25 | drivers := []struct { 26 | driver string 27 | dsn string 28 | create string 29 | cleanup func(db *DB) error 30 | }{ 31 | {driver: "sqlite3", 32 | dsn: sqliteTestFile, 33 | create: ` 34 | CREATE TABLE users ( 35 | id INTEGER PRIMARY KEY, 36 | created DATETIME DEFAULT CURRENT_TIMESTAMP, 37 | name VARCHAR(128) NOT NULL 38 | )`, 39 | cleanup: func(*DB) error { return os.Remove(sqliteTestFile) }}, 40 | {driver: "mysql", 41 | dsn: "root:@/sequel_test?parseTime=true", 42 | create: ` 43 | CREATE TABLE users ( 44 | id INTEGER PRIMARY KEY AUTO_INCREMENT, 45 | created DATETIME DEFAULT CURRENT_TIMESTAMP, 46 | name VARCHAR(128) NOT NULL 47 | )`, 48 | cleanup: func(db *DB) error { 49 | _, _ = db.Exec(`DROP TABLE users`) 50 | return nil 51 | }, 52 | }, 53 | {driver: "postgres", 54 | dsn: "dbname=sequel_test sslmode=disable", 55 | create: ` 56 | CREATE TABLE users ( 57 | id SERIAL PRIMARY KEY, 58 | created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 59 | name VARCHAR(128) NOT NULL 60 | )`, 61 | cleanup: func(db *DB) error { 62 | _, _ = db.Exec(`DROP TABLE users`) 63 | return nil 64 | }, 65 | }, 66 | } 67 | 68 | insertSlice := func(t *testing.T, db *DB) []*User { 69 | users := []*User{{Name: "Alice"}, {Name: "Bob"}} 70 | ids, err := db.Insert("users", users) 71 | require.NoError(t, err) 72 | require.Len(t, ids, 2) 73 | require.Equal(t, int(ids[0]), users[0].ID) 74 | require.Equal(t, int(ids[1]), users[1].ID) 75 | return users 76 | } 77 | 78 | normaliseUsers := func(users []*User) { 79 | for _, user := range users { 80 | user.Created = time.Time{} 81 | } 82 | } 83 | 84 | tests := []struct { 85 | name string 86 | test func(t *testing.T, db *DB) 87 | }{ 88 | {"InsertOne", func(t *testing.T, db *DB) { 89 | user := &User{Name: "Bob"} 90 | ids, err := db.Insert("users", user) 91 | require.NoError(t, err) 92 | require.Len(t, ids, 1) 93 | require.Equal(t, int(ids[0]), user.ID) 94 | }}, 95 | {"InsertSlice", func(t *testing.T, db *DB) { 96 | insertSlice(t, db) 97 | }}, 98 | {"SelectOne", func(t *testing.T, db *DB) { 99 | insertSlice(t, db) 100 | user := &User{} 101 | err := db.SelectOne(user, `SELECT ** FROM users WHERE name = ?`, "Alice") 102 | require.NoError(t, err) 103 | require.Equal(t, "Alice", user.Name) 104 | }}, 105 | {"Select", func(t *testing.T, db *DB) { 106 | expected := insertSlice(t, db) 107 | actual := []*User{} 108 | err := db.Select(&actual, `SELECT ** FROM users ORDER BY name`) 109 | require.NoError(t, err) 110 | normaliseUsers(actual) 111 | require.Equal(t, expected, actual) 112 | }}, 113 | {"Upsert", func(t *testing.T, db *DB) { 114 | users := insertSlice(t, db) 115 | users[0].Name = "Alex" 116 | users[0].Created = time.Now() 117 | _, err := db.Upsert("users", []string{"id"}, users[0]) 118 | require.NoError(t, err) 119 | 120 | actual := []*User{} 121 | err = db.Select(&actual, `SELECT ** FROM users ORDER BY name`) 122 | require.NoError(t, err) 123 | normaliseUsers(users) 124 | normaliseUsers(actual) 125 | require.Equal(t, users, actual) 126 | }}, 127 | } 128 | for _, driver := range drivers { 129 | t.Run(driver.driver, func(t *testing.T) { 130 | for _, test := range tests { 131 | t.Run(test.name, func(t *testing.T) { 132 | db, err := Open(driver.driver, driver.dsn) 133 | require.NoError(t, err) 134 | defer func() { 135 | _ = driver.cleanup(db) 136 | _ = db.Close() 137 | }() 138 | 139 | if driver.cleanup != nil { 140 | _ = driver.cleanup(db) 141 | } 142 | 143 | _, err = db.Exec(driver.create) 144 | require.NoError(t, err) 145 | 146 | test.test(t, db) 147 | }) 148 | } 149 | }) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /dialect_test.go: -------------------------------------------------------------------------------- 1 | package sequel 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | type TestMetadata struct { 10 | Email string 11 | Age int 12 | } 13 | 14 | type TestUser struct { 15 | ID int `db:",managed"` 16 | Name string 17 | TestMetadata 18 | } 19 | 20 | func TestDialectExpand(t *testing.T) { 21 | type dialectResult struct { 22 | dialect dialect 23 | query string 24 | args []interface{} 25 | } 26 | 27 | // Note that the sqlite dialect is not currently tested as its dialect is identical to MySQL. 28 | // If the dialects diverge this will change. 29 | tests := []struct { 30 | name string 31 | query string 32 | args []interface{} 33 | expected []dialectResult 34 | }{ 35 | { 36 | name: "Scalars", 37 | query: `INSERT INTO user (name, age, email) VALUES (?, ?, ?)`, 38 | args: []interface{}{"Moe", 39, "moe@stooges.com"}, 39 | expected: []dialectResult{ 40 | { 41 | dialect: dialects["postgres"], 42 | args: []interface{}{"Moe", 39, "moe@stooges.com"}, 43 | query: `INSERT INTO user (name, age, email) VALUES ($1, $2, $3)`, 44 | }, 45 | { 46 | dialect: dialects["mysql"], 47 | args: []interface{}{"Moe", 39, "moe@stooges.com"}, 48 | query: `INSERT INTO user (name, age, email) VALUES (?, ?, ?)`, 49 | }, 50 | }, 51 | }, 52 | { 53 | name: "Struct", 54 | query: `INSERT INTO user (name, age, email) VALUES ?`, 55 | args: []interface{}{struct { 56 | Name string 57 | Age int 58 | Email string 59 | }{"Moe", 39, "moe@stooges.com"}}, 60 | expected: []dialectResult{ 61 | { 62 | dialect: dialects["postgres"], 63 | args: []interface{}{"Moe", 39, "moe@stooges.com"}, 64 | query: `INSERT INTO user (name, age, email) VALUES ($1, $2, $3)`, 65 | }, 66 | { 67 | dialect: dialects["mysql"], 68 | args: []interface{}{"Moe", 39, "moe@stooges.com"}, 69 | query: `INSERT INTO user (name, age, email) VALUES (?, ?, ?)`, 70 | }, 71 | }, 72 | }, 73 | { 74 | name: "SliceOfStructs", 75 | query: `INSERT INTO user VALUES ?`, 76 | args: []interface{}{[]struct { 77 | Age int 78 | Name string 79 | }{ 80 | {43, "Moe"}, 81 | {39, "Curly"}, 82 | }}, 83 | expected: []dialectResult{ 84 | { 85 | dialect: dialects["postgres"], 86 | query: `INSERT INTO user VALUES ($1, $2), ($3, $4)`, 87 | args: []interface{}{43, "Moe", 39, "Curly"}, 88 | }, 89 | { 90 | dialect: dialects["mysql"], 91 | query: `INSERT INTO user VALUES (?, ?), (?, ?)`, 92 | args: []interface{}{43, "Moe", 39, "Curly"}, 93 | }, 94 | }, 95 | }, 96 | { 97 | name: "EmbeddedStruct", 98 | query: `INSERT INTO table VALUES ?`, 99 | args: []interface{}{ 100 | []TestUser{ 101 | { 102 | ID: 2, 103 | Name: "Moe", 104 | TestMetadata: TestMetadata{ 105 | Email: "moe@stooges.com", 106 | Age: 39, 107 | }, 108 | }, 109 | { 110 | ID: 3, 111 | Name: "Curly", 112 | TestMetadata: TestMetadata{ 113 | Email: "curly@stooges.com", 114 | Age: 39, 115 | }, 116 | }, 117 | }, 118 | }, 119 | expected: []dialectResult{ 120 | { 121 | dialect: dialects["postgres"], 122 | query: `INSERT INTO table VALUES ($1, $2, $3, $4), ($5, $6, $7, $8)`, 123 | args: []interface{}{2, "Moe", "moe@stooges.com", 39, 3, "Curly", "curly@stooges.com", 39}, 124 | }, 125 | { 126 | dialect: dialects["mysql"], 127 | query: `INSERT INTO table VALUES (?, ?, ?, ?), (?, ?, ?, ?)`, 128 | args: []interface{}{2, "Moe", "moe@stooges.com", 39, 3, "Curly", "curly@stooges.com", 39}, 129 | }, 130 | }, 131 | }, 132 | } 133 | for _, test := range tests { 134 | // nolint: scopelint 135 | t.Run(test.name, func(t *testing.T) { 136 | for _, result := range test.expected { 137 | t.Run(result.dialect.Name(), func(t *testing.T) { 138 | query, args, err := expand(result.dialect, true, nil, test.query, test.args) 139 | require.NoError(t, err, "%q", test.query) 140 | require.Equal(t, result.query, query) 141 | require.Equal(t, result.args, args) 142 | }) 143 | } 144 | }) 145 | } 146 | } 147 | 148 | func TestDialectExpandSelect(t *testing.T) { 149 | dest := []TestUser{} 150 | builder, err := makeRowBuilderForSlice(&dest) 151 | require.NoError(t, err) 152 | query, args, err := expand(dialects["postgres"], true, builder, `SELECT ** FROM test`, []interface{}{dest}) 153 | require.NoError(t, err) 154 | require.Equal(t, `SELECT "id", "name", "email", "age" FROM test`, query) 155 | require.Empty(t, args) 156 | } 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sequel - A Go <-> SQL mapping package (Status: ALPHA) [![](https://godoc.org/github.com/alecthomas/sequel?status.svg)](http://godoc.org/github.com/alecthomas/sequel) [![CircleCI](https://img.shields.io/circleci/project/github/alecthomas/sequel.svg)](https://circleci.com/gh/alecthomas/sequel) 2 | 3 | Sequel is similar to SQLx, but with the goal of automating even more of the common 4 | operations around Go <-> SQL interaction. 5 | 6 | ## Why? 7 | 8 | I wanted a very thin mapping between SQL and Go that provides: 9 | 10 | 1. `SELECT` into arbitrary `struct`s. 11 | 2. Query parameters populated from arbitrary Go types - structs, slices, etc. 12 | 3. Normalised sequential placeholders across SQL dialects (`?`) (support for positional placeholders will hopefully come later). 13 | 4. Try to be as type safe as possible. 14 | 15 | I did not want: 16 | 17 | 1. A query DSL - I already know SQL. 18 | 2. Migration support - there are much better external tools for this. 19 | 20 | ## Tutorial / example 21 | 22 | Open a DB connection: 23 | 24 | ```go 25 | db, err := sequel.Open("mysql", "root@/database") 26 | ``` 27 | 28 | Insert some users: 29 | 30 | ```go 31 | type dbUser struct { 32 | ID int `db:",managed"` 33 | Created time.Time `db:",managed"` 34 | Name string 35 | Email string 36 | } 37 | 38 | users := []dbUser{ 39 | {Name: "Moe", Email: "moe@stooges.com"}, 40 | {Name: "Larry", Email: "larry@stooges.com"}, 41 | {Name: "Curly", Email: "curly@stooges.com"}, 42 | } 43 | _, err = db.Insert("users", users) 44 | ``` 45 | 46 | Selecting uses a similar approach: 47 | 48 | ```go 49 | users := []dbUser{} 50 | err = db.Select(&users, ` 51 | SELECT ** FROM users WHERE id IN ( 52 | SELECT user_id FROM group_members WHERE group_id = ? 53 | ) 54 | `, groupID) 55 | ``` 56 | 57 | ## Placeholders 58 | 59 | Each placeholder symbol `?` in a query string maps 1:1 to a corresponding argument in the `Select()` or `Exec()` call. 60 | 61 | The placeholder `**` will expand to the set of *unmanaged* fields in your data model. Managed fields are those 62 | managed by the database, such as auto-increment keys, fields with auto-update values, etc. See section 63 | below on "Dealing with schema changes" for why this placeholder is useful. 64 | 65 | Arguments are expanded recursively. Structs map to a parentheses-enclosed, comma-separated list. Slices map to a comma-separated list. 66 | 67 | Value | Placeholder | Corresponding expansion 68 | ------------------------------------------------|-------------|------------------------- 69 | `struct{A, B, C string}{"A", "B", "C"}` | `?` | `(?, ?, ?)` 70 | `[]string{"A", "B"}` | `?` | `?, ?` 71 | `[]struct{A, B string}{{"A", "B"}, {"C", "D"}}` | `?` | `(?, ?), (?, ?)` 72 | `struct{A, B, C string}{"A", "B", "C"}` | `**` | `a, b, c` 73 | 74 | ## Struct tag format 75 | 76 | Struct fields may be tagged with `db:"..."` to control how Sequel maps fields. The tag has the following 77 | syntax: 78 | 79 | db:"[][,