Skip to content

Commit

Permalink
feat: Add SQL Server driver
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelespinoza committed Apr 30, 2022
1 parent 97d4847 commit 6c773a9
Show file tree
Hide file tree
Showing 15 changed files with 334 additions and 2 deletions.
13 changes: 13 additions & 0 deletions .ci/sqlserver/client.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM godfish_test/client_base:latest

WORKDIR /src
RUN make build-sqlserver

# Alpine linux doesn't have a SQL Server client. Build a golang binary to
# check if server is ready. Use it in the entrypoint.
WORKDIR /src/.ci/sqlserver
RUN go build -v -o /client_check_db .
COPY .ci/sqlserver/client.sh /

WORKDIR /src
ENTRYPOINT /client.sh
37 changes: 37 additions & 0 deletions .ci/sqlserver/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
"database/sql"
"log"
"os"

// Imported for the side effect of registering the driver.
_ "github.com/denisenkom/go-mssqldb"
)

var dsn string

func init() {
log.SetOutput(os.Stderr)

const key = "DB_DSN"
if dsn = os.Getenv(key); dsn == "" {
log.Fatalf("missing required env var %q", key)
}
}

func main() {
if err := ping(); err != nil {
log.Fatal(err)
}
log.Println("ok")
}

func ping() error {
conn, err := sql.Open("sqlserver", dsn)
if err != nil {
return err
}
defer conn.Close()
return conn.Ping()
}
27 changes: 27 additions & 0 deletions .ci/sqlserver/client.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env sh

set -eu

echo "building binary"
make build-sqlserver
echo "testing godfish"
make test ARGS='-v -count=1 -coverprofile=/tmp/cover.out'

# Wait for db server to be ready, with some limits.

num_attempts=0

until /client_check_db ; do
num_attempts=$((num_attempts+1))
if [ $num_attempts -ge 15 ]; then
>&2 echo "ERROR: max attempts exceeded"
exit 1
fi

>&2 echo "db is unavailable now, sleeping"
sleep 2
done
>&2 echo "db is up"

echo "testing godfish against live db"
make test-sqlserver ARGS='-v -count=1 -coverprofile=/tmp/cover_driver.out'
27 changes: 27 additions & 0 deletions .ci/sqlserver/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
version: "3.9"

services:
client:
build:
context: "${BUILD_DIR}"
dockerfile: "${BUILD_DIR}/.ci/sqlserver/client.Dockerfile"
image: godfish_test/sqlserver/client:latest
container_name: godfish_ci_sqlserver_client
depends_on:
- server
entrypoint: /client.sh
env_file:
- env
environment:
CGO_ENABLED: 0
tty: true
server:
build:
context: "${BUILD_DIR}"
dockerfile: "${BUILD_DIR}/.ci/sqlserver/server.Dockerfile"
image: godfish_test/sqlserver/server:latest
container_name: godfish_ci_sqlserver_server
env_file:
- env
environment:
ACCEPT_EULA: '1'
2 changes: 2 additions & 0 deletions .ci/sqlserver/env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SA_PASSWORD=1_complicated_password!
DB_DSN="sqlserver://sa:${SA_PASSWORD}@server"
1 change: 1 addition & 0 deletions .ci/sqlserver/server.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM mcr.microsoft.com/mssql/server
18 changes: 18 additions & 0 deletions .github/workflows/build-sqlserver.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: sqlserver
on: [push, pull_request]
jobs:
all:
runs-on: ubuntu-20.04
steps:
- name: Checkout repo
uses: actions/checkout@v2
- name: Build environment and run tests
run: make -f ci.Makefile ci-sqlserver-up
- name: Upload code coverage
uses: codecov/codecov-action@v2
with:
fail_ci_if_error: true
files: /tmp/cover.out,/tmp/cover_driver.out
verbose: true
- name: Teardown
run: make -f ci.Makefile ci-sqlserver-down
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ GOSEC ?= gosec

CORE_SRC_PKG_PATHS=$(PKG_IMPORT_PATH) $(PKG_IMPORT_PATH)/internal/...
CASSANDRA_PATH=$(PKG_IMPORT_PATH)/drivers/cassandra
SQLSERVER_PATH=$(PKG_IMPORT_PATH)/drivers/sqlserver
MYSQL_PATH=$(PKG_IMPORT_PATH)/drivers/mysql
POSTGRES_PATH=$(PKG_IMPORT_PATH)/drivers/postgres
SQLITE3_PATH=$(PKG_IMPORT_PATH)/drivers/sqlite3
Expand Down Expand Up @@ -67,6 +68,19 @@ build-postgres: _mkdir
test-postgres:
$(GO) test $(ARGS) $(POSTGRES_PATH)/...

#
# Microsoft SQL Server
#
build-sqlserver: BIN=$(BIN_DIR)/godfish_sqlserver
build-sqlserver:
$(GO) build -o $(BIN) -v \
-ldflags "$(LDFLAGS) \
-X $(PKG_IMPORT_PATH)/internal/cmd.versionDriver=sqlserver" \
$(SQLSERVER_PATH)/godfish
@echo "built sqlserver to $(BIN)"
test-sqlserver:
$(GO) test $(ARGS) $(SQLSERVER_PATH)/...

#
# MySQL
#
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
[![mysql](https://github.com/rafaelespinoza/godfish/actions/workflows/build-mysql.yml/badge.svg)](https://github.com/rafaelespinoza/godfish/actions/workflows/build-mysql.yml)
[![postgres](https://github.com/rafaelespinoza/godfish/actions/workflows/build-postgres.yml/badge.svg)](https://github.com/rafaelespinoza/godfish/actions/workflows/build-postgres.yml)
[![sqlite3](https://github.com/rafaelespinoza/godfish/actions/workflows/build-sqlite3.yml/badge.svg)](https://github.com/rafaelespinoza/godfish/actions/workflows/build-sqlite3.yml)
[![sqlserver](https://github.com/rafaelespinoza/godfish/actions/workflows/build-sqlserver.yml/badge.svg)](https://github.com/rafaelespinoza/godfish/actions/workflows/sqlserver.yml)

`godfish` is a database migration manager, similar to the very good
[`dogfish`](https://github.com/dwb/dogfish), but written in golang.
Expand All @@ -29,6 +30,7 @@ make build-cassandra
make build-mysql
make build-postgres
make build-sqlite3
make build-sqlserver
```

From there you could move it to `$GOPATH/bin`, move it to your project or
Expand Down Expand Up @@ -154,24 +156,36 @@ Docker and docker-compose are used to create environments and run the tests
against a live database. Each database has a separate configuration. All of this
lives in `ci.Makefile` and the `.ci/` directory.

Build environments and run tests
```sh
# Build environments and run tests
make -f ci.Makefile ci-cassandra3-up
make -f ci.Makefile ci-cassandra4-up

make -f ci.Makefile ci-sqlserver-up

make -f ci.Makefile ci-mariadb-up
make -f ci.Makefile ci-mysql57-up
make -f ci.Makefile ci-mysql8-up

make -f ci.Makefile ci-postgres12-up
make -f ci.Makefile ci-postgres13-up

make -f ci.Makefile ci-sqlite3-up
```

# Teardown
Teardown
```sh
make -f ci.Makefile ci-cassandra3-down
make -f ci.Makefile ci-cassandra4-down

make -f ci.Makefile ci-sqlserver-down

make -f ci.Makefile ci-mariadb-down
make -f ci.Makefile ci-mysql57-down
make -f ci.Makefile ci-mysql8-down

make -f ci.Makefile ci-postgres12-down
make -f ci.Makefile ci-postgres13-down

make -f ci.Makefile ci-sqlite3-down
```
7 changes: 7 additions & 0 deletions ci.Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ ci-sqlite3-up: build-base
ci-sqlite3-down:
docker-compose -f $(SQLITE3_FILE) down --rmi all --volumes

SQLSERVER_FILE=$(CI_DIR)/sqlserver/docker-compose.yml
ci-sqlserver-up: build-base
BUILD_DIR=$(BUILD_DIR) docker-compose -f $(SQLSERVER_FILE) up --build --exit-code-from client && \
.ci/cp_coverage_to_host.sh $(SQLSERVER_FILE)
ci-sqlserver-down:
docker-compose -f $(SQLSERVER_FILE) down --rmi all --volumes

#
# Build and tag base image.
#
Expand Down
18 changes: 18 additions & 0 deletions drivers/sqlserver/godfish/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package main

import (
"context"
"log"
"os"

"github.com/rafaelespinoza/godfish/drivers/sqlserver"
"github.com/rafaelespinoza/godfish/internal/cmd"
)

func main() {
root := cmd.New(sqlserver.NewDriver())
if err := root.Run(context.TODO(), os.Args[1:]); err != nil {
log.Println(err)
os.Exit(1)
}
}
91 changes: 91 additions & 0 deletions drivers/sqlserver/sqlserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package sqlserver

import (
"database/sql"
"errors"
"strings"

mssql "github.com/denisenkom/go-mssqldb"
"github.com/rafaelespinoza/godfish"
)

// NewDriver creates a new Microsoft SQL Server driver.
func NewDriver() godfish.Driver { return &driver{} }

// driver implements the godfish.Driver interface for Microsoft SQL Server.
type driver struct {
connection *sql.DB
}

func (d *driver) Name() string { return "sqlserver" }
func (d *driver) Connect(dsn string) (err error) {
if d.connection != nil {
return
}

// github.com/denisenkom/go-mssqldb registers two sql.Driver names:
// "mssql" and "sqlserver". Their docs seem to steer users towards the
// latter, so just use that one.
conn, err := sql.Open("sqlserver", dsn)
if err != nil {
return
}
d.connection = conn
return
}

func (d *driver) Close() (err error) {
conn := d.connection
if conn == nil {
return
}
d.connection = nil
err = conn.Close()
return
}

func (d *driver) Execute(query string, args ...interface{}) (err error) {
_, err = d.connection.Exec(query)
return
}

func (d *driver) CreateSchemaMigrationsTable() (err error) {
_, err = d.connection.Exec(`
IF NOT EXISTS (
SELECT 1 FROM information_schema.tables WHERE table_schema = (SELECT schema_name()) AND table_name = 'schema_migrations'
)
CREATE TABLE schema_migrations (migration_id VARCHAR(128) PRIMARY KEY NOT NULL)
`)
return
}

func (d *driver) AppliedVersions() (out godfish.AppliedVersions, err error) {
rows, err := d.connection.Query(`SELECT migration_id FROM schema_migrations ORDER BY migration_id ASC`)

var ierr mssql.Error
// https://docs.microsoft.com/en-us/sql/relational-databases/errors-events/database-engine-events-and-errors
// Invalid object name 'schema_migrations'
if errors.As(err, &ierr) && ierr.SQLErrorNumber() == 208 && strings.Contains(ierr.Error(), "schema_migrations") {
err = godfish.ErrSchemaMigrationsDoesNotExist
}
out = godfish.AppliedVersions(rows)
return
}

func (d *driver) UpdateSchemaMigrations(forward bool, version string) (err error) {
conn := d.connection
if forward {
_, err = conn.Exec(`
INSERT INTO schema_migrations (migration_id)
VALUES (@p1)`,
version,
)
} else {
_, err = conn.Exec(`
DELETE FROM schema_migrations
WHERE migration_id = @p1`,
version,
)
}
return
}
27 changes: 27 additions & 0 deletions drivers/sqlserver/sqlserver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package sqlserver_test

import (
"testing"

"github.com/rafaelespinoza/godfish/drivers/sqlserver"
"github.com/rafaelespinoza/godfish/internal/test"
)

func Test(t *testing.T) {
queries := test.Queries{
CreateFoos: test.MigrationContent{
Forward: "CREATE TABLE foos (id int PRIMARY KEY);",
Reverse: "DROP TABLE foos;",
},
CreateBars: test.MigrationContent{
Forward: "CREATE TABLE bars (id int PRIMARY KEY);",
Reverse: "DROP TABLE bars;",
},
AlterFoos: test.MigrationContent{
Forward: `ALTER TABLE foos ADD a varchar(255);`,
Reverse: "ALTER TABLE foos DROP COLUMN a;",
},
}

test.RunDriverTests(t, sqlserver.NewDriver(), queries)
}
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/rafaelespinoza/godfish
go 1.17

require (
github.com/denisenkom/go-mssqldb v0.12.0
github.com/go-sql-driver/mysql v1.5.0
github.com/gocql/gocql v0.0.0-20211222173705-d73e6b1002a7
github.com/lib/pq v1.10.0
Expand All @@ -11,7 +12,10 @@ require (
)

require (
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
)

0 comments on commit 6c773a9

Please sign in to comment.