Skip to content

Commit

Permalink
Add support for SingleStore (#26)
Browse files Browse the repository at this point in the history
* switch coverage to go1.18

* add support for SingleStore

* small increase in coverage
  • Loading branch information
muir authored Oct 28, 2022
1 parent fa49111 commit 7e44bd3
Show file tree
Hide file tree
Showing 9 changed files with 672 additions and 43 deletions.
53 changes: 53 additions & 0 deletions .github/workflows/s2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: SingleStore tests
on: [ push ]

jobs:
Test-mysql-integration:
runs-on: ubuntu-latest

environment: singlestore

services:
singlestoredb:
image: ghcr.io/singlestore-labs/singlestoredb-dev
ports:
- 3306:3306
- 8080:8080
- 9000:9000
env:
ROOT_PASSWORD: test
SINGLESTORE_LICENSE: ${{ secrets.SINGLESTORE_LICENSE }}

steps:
- name: sanity check using mysql client
run: |
mysql -u root -ptest -e "CREATE DATABASE libschematest" -h 127.0.0.1
- name: Check out repository code
uses: actions/checkout@v2

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16

- name: Build
run: go build -v ./...

- name: Test
env:
LIBSCHEMA_SINGLESTORE_TEST_DSN: "root:test@tcp(127.0.0.1:3306)/libschematest?tls=false"
run: go test ./lsmysql/... ./lssinglestore/... -v

- name: Run Coverage
env:
LIBSCHEMA_SINGLESTORE_TEST_DSN: "root:test@tcp(127.0.0.1:3306)/libschematest?tls=false"
run: go test -coverprofile=coverage.txt -covermode=atomic -coverpkg=github.com/muir/libschema/... ./lsmysql/... ./lssinglestore/...

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
with:
verbose: true
flags: singlestore_tests
fail_ci_if_error: true

17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

# libschema - schema migration for libraries
# libschema - database schema migration for libraries

[![GoDoc](https://godoc.org/github.com/muir/libschema?status.png)](https://pkg.go.dev/github.com/muir/libschema)
![unit tests](https://github.com/muir/libschema/actions/workflows/go.yml/badge.svg)
Expand All @@ -17,7 +17,7 @@ Install:

## Libraries

Libschema provides a way for Go libraries to manage their own migrations.
Libschema provides a way for Go libraries to manage their own database migrations.

Trying migrations to libraries supports two things: the first is
source code locality: the migrations can be next to the code that
Expand Down Expand Up @@ -123,7 +123,7 @@ database.Migrations("users",
ADD COLUMN org TEXT,
ADD ADD CONSTRAINT orgfk FOREIGN KEY (org)
REFERENCES org (name) `,
libschema.After("orgs", "createOrgTable")),
libschema.After("orgs", "createOrgTable")),
)

database.Migrations("orgs",
Expand Down Expand Up @@ -180,11 +180,14 @@ be given their own hook.

## Driver inclusion and database support

Like database/sql, libschema requires database-specific drivers.
Include "github.com/muir/libschema/lspostgres" for Postgres support
and "github.com/muir/libschema/lsmysql" for Mysql support.
Like database/sql, libschema requires database-specific drivers:

libschema currently supports: PostgreSQL, MySQL. It is relatively easy to add additional databases.
- PostgreSQL support is in "github.com/muir/libschema/lspostgres"
- MySQL support in "github.com/muir/libschema/lsmysql"
- SingleStore support "github.com/muir/libschema/lssinglestore"

libschema currently supports: PostgreSQL, SingleStore, MySQL.
It is relatively easy to add additional databases.

## Forward only

Expand Down
100 changes: 78 additions & 22 deletions lsmysql/mysql.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package lsmysql has a libschema.Driver support MySQL
package lsmysql

import (
Expand All @@ -6,6 +7,7 @@ import (
"fmt"
"regexp"
"strings"
"sync"

"github.com/muir/libschema"
"github.com/muir/libschema/internal"
Expand All @@ -18,6 +20,8 @@ import (
// * CANNOT do DDL commands inside transactions
// * Support UPSERT using INSERT ... ON DUPLICATE KEY UPDATE
// * uses /* -- and # for comments
// * supports advisory locks
// * has quoting modes (ANSI_QUOTES)
//
// Because mysql DDL commands cause transactions to autocommit, tracking the schema changes in
// a secondary table (like libschema does) is inherently unsafe. The MySQL driver will
Expand All @@ -36,18 +40,42 @@ import (
// be propagated into the MySQL object and be used as a default table for all of the
// functions to interrogate data defintion status.
type MySQL struct {
lockTx *sql.Tx
lockStr string
db *sql.DB
databaseName string // used in skip.go only
lockTx *sql.Tx
lockStr string
db *sql.DB
databaseName string // used in skip.go only
lock sync.Mutex
trackingSchemaTable func(*libschema.Database) (string, string, error)
skipDatabase bool
}

type MySQLOpt func(*MySQL)

// WithoutDatabase skips creating a *libschema.Database. Without it,
// functions for getting and setting the dbNames are required.
func WithoutDatabase(p *MySQL) {
p.skipDatabase = true
}

// New creates a libschema.Database with a mysql driver built in.
func New(log *internal.Log, name string, schema *libschema.Schema, db *sql.DB) (*libschema.Database, *MySQL, error) {
m := &MySQL{db: db}
d, err := schema.NewDatabase(log, name, db, m)
m.databaseName = d.Options.SchemaOverride
return d, m, err
func New(log *internal.Log, name string, schema *libschema.Schema, db *sql.DB, options ...MySQLOpt) (*libschema.Database, *MySQL, error) {
m := &MySQL{
db: db,
trackingSchemaTable: trackingSchemaTable,
}
for _, opt := range options {
opt(m)
}
var d *libschema.Database
if !m.skipDatabase {
var err error
d, err = schema.NewDatabase(log, name, db, m)
if err != nil {
return nil, nil, err
}
m.databaseName = d.Options.SchemaOverride
}
return d, m, nil
}

type mmigration struct {
Expand Down Expand Up @@ -115,7 +143,9 @@ func (m mmigration) applyOpts(opts []libschema.MigrationOption) libschema.Migrat
}

// DoOneMigration applies a single migration.
// It is expected to be called by libschema.
// It is expected to be called by libschema and is not
// called internally which means that is safe to override
// in types that embed MySQL.
func (p *MySQL) DoOneMigration(ctx context.Context, log *internal.Log, d *libschema.Database, m libschema.Migration) (result sql.Result, err error) {
// TODO: DRY
defer func() {
Expand Down Expand Up @@ -185,9 +215,11 @@ func (p *MySQL) DoOneMigration(ctx context.Context, log *internal.Log, d *libsch
}

// CreateSchemaTableIfNotExists creates the migration tracking table for libschema.
// It is expected to be called by libschema.
// It is expected to be called by libschema and is not
// called internally which means that is safe to override
// in types that embed MySQL.
func (p *MySQL) CreateSchemaTableIfNotExists(ctx context.Context, _ *internal.Log, d *libschema.Database) error {
schema, tableName, err := trackingSchemaTable(d)
schema, tableName, err := p.trackingSchemaTable(d)
if err != nil {
return err
}
Expand Down Expand Up @@ -216,6 +248,12 @@ func (p *MySQL) CreateSchemaTableIfNotExists(ctx context.Context, _ *internal.Lo

var simpleIdentifierRE = regexp.MustCompile(`\A[A-Za-z][A-Za-z0-9_]*\z`)

func WithTrackingTableQuoter(f func(*libschema.Database) (schemaName string, tableName string, err error)) MySQLOpt {
return func(p *MySQL) {
p.trackingSchemaTable = f
}
}

// When MySQL is in ANSI_QUOTES mode, it allows "table_name" quotes but when
// it is not then it does not. There is no prefect option: in ANSI_QUOTES
// mode, you could have a table called `table` (eg: `CREATE TABLE "table"`) but
Expand Down Expand Up @@ -247,9 +285,8 @@ func trackingSchemaTable(d *libschema.Database) (string, string, error) {

// trackingTable returns the schema+table reference for the migration tracking table.
// The name is already quoted properly for use as a save mysql identifier.
// TODO: DRY
func trackingTable(d *libschema.Database) string {
_, table, _ := trackingSchemaTable(d)
func (p *MySQL) trackingTable(d *libschema.Database) string {
_, table, _ := p.trackingSchemaTable(d)
return table
}

Expand All @@ -265,7 +302,7 @@ func (p *MySQL) saveStatus(log *internal.Log, tx *sql.Tx, d *libschema.Database,
})
q := fmt.Sprintf(`
REPLACE INTO %s (library, migration, done, error, updated_at)
VALUES (?, ?, ?, ?, now())`, trackingTable(d))
VALUES (?, ?, ?, ?, now())`, p.trackingTable(d))
_, err := tx.Exec(q, m.Base().Name.Library, m.Base().Name.Name, done, estr)
if err != nil {
return errors.Wrapf(err, "Save status for %s", m.Base().Name)
Expand All @@ -275,13 +312,20 @@ func (p *MySQL) saveStatus(log *internal.Log, tx *sql.Tx, d *libschema.Database,

// LockMigrationsTable locks the migration tracking table for exclusive use by the
// migrations running now.
// It is expected to be called by libschema.
//
// It is expected to be called by libschema and is not
// called internally which means that is safe to override
// in types that embed MySQL.
//
// In MySQL, locks are _not_ tied to transactions so closing the transaction
// does not release the lock. We'll use a transaction just to make sure that
// we're using the same connection. If LockMigrationsTable succeeds, be sure to
// call UnlockMigrationsTable.
func (p *MySQL) LockMigrationsTable(ctx context.Context, _ *internal.Log, d *libschema.Database) error {
_, tableName, err := trackingSchemaTable(d)
// LockMigrationsTable is overridden for SingleStore
p.lock.Lock()
defer p.lock.Unlock()
_, tableName, err := p.trackingSchemaTable(d)
if err != nil {
return err
}
Expand All @@ -303,8 +347,14 @@ func (p *MySQL) LockMigrationsTable(ctx context.Context, _ *internal.Log, d *lib
}

// UnlockMigrationsTable unlocks the migration tracking table.
// It is expected to be called by libschema.
//
// It is expected to be called by libschema and is not
// called internally which means that is safe to override
// in types that embed MySQL.
func (p *MySQL) UnlockMigrationsTable(_ *internal.Log) error {
// UnlockMigrationsTable is overridden for SingleStore
p.lock.Lock()
defer p.lock.Unlock()
if p.lockTx == nil {
return errors.Errorf("libschema migrations table, not locked")
}
Expand All @@ -320,10 +370,13 @@ func (p *MySQL) UnlockMigrationsTable(_ *internal.Log) error {
}

// LoadStatus loads the current status of all migrations from the migration tracking table.
// It is expected to be called by libschema.
//
// It is expected to be called by libschema and is not
// called internally which means that is safe to override
// in types that embed MySQL.
func (p *MySQL) LoadStatus(ctx context.Context, _ *internal.Log, d *libschema.Database) ([]libschema.MigrationName, error) {
// TODO: DRY
tableName := trackingTable(d)
tableName := p.trackingTable(d)
rows, err := d.DB().QueryContext(ctx, fmt.Sprintf(`
SELECT library, migration, done
FROM %s`, tableName))
Expand Down Expand Up @@ -352,7 +405,10 @@ func (p *MySQL) LoadStatus(ctx context.Context, _ *internal.Log, d *libschema.Da

// IsMigrationSupported checks to see if a migration is well-formed. Absent a code change, this
// should always return nil.
// It is expected to be called by libschema.
//
// It is expected to be called by libschema and is not
// called internally which means that is safe to override
// in types that embed MySQL.
func (p *MySQL) IsMigrationSupported(d *libschema.Database, _ *internal.Log, migration libschema.Migration) error {
m, ok := migration.(*mmigration)
if !ok {
Expand Down
Loading

0 comments on commit 7e44bd3

Please sign in to comment.