Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for SingleStore #26

Merged
merged 3 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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