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

Turso support #581

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ For a comparison between dbmate and other popular database schema migration tool
- [ClickHouse](#clickhouse)
- [BigQuery](#bigquery)
- [Spanner](#spanner)
- [Turso](#turso)
- [Creating Migrations](#creating-migrations)
- [Running Migrations](#running-migrations)
- [Rolling Back Migrations](#rolling-back-migrations)
Expand Down Expand Up @@ -327,6 +328,18 @@ DROP TABLE ...

Schema dumps are not currently supported, as `pg_dump` uses functions that are not provided by Spanner.

#### Turso

Turso implementation currently supports migrations but not database creation and drop.

[Turso cli](https://docs.turso.tech/cli/installation) is requred for everything to work as expected.

Follow format below for `DATABASE_URL`:

```shell
DATABASE_URL="turso://[databaseName]-[organizationName].turso.io?authToken=[token]
```

### Creating Migrations

To create a new migration, run `dbmate new create_users_table`. You can name the migration anything you like. This will create a file `db/migrations/20151127184807_create_users_table.sql` in the current directory:
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/ClickHouse/ch-go v0.62.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/apache/arrow/go/v15 v15.0.2 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
Expand All @@ -54,6 +56,7 @@ require (
github.com/segmentio/asm v1.2.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.opencensus.io v0.24.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE=
github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
Expand All @@ -217,6 +219,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -425,6 +429,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU=
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
_ "github.com/amacneil/dbmate/v2/pkg/driver/clickhouse"
_ "github.com/amacneil/dbmate/v2/pkg/driver/mysql"
_ "github.com/amacneil/dbmate/v2/pkg/driver/postgres"
_ "github.com/amacneil/dbmate/v2/pkg/driver/turso"
)

func main() {
Expand Down
176 changes: 176 additions & 0 deletions pkg/driver/turso/turso.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package turso

import (
"bytes"
"database/sql"
"fmt"
"io"
"net/url"
"strings"

"github.com/amacneil/dbmate/v2/pkg/dbmate"
"github.com/amacneil/dbmate/v2/pkg/dbutil"

_ "github.com/tursodatabase/libsql-client-go/libsql"
)

func init() {
dbmate.RegisterDriver(NewDriver, "turso")
}

type Driver struct {
migrationsTableName string
databaseURL *url.URL
log io.Writer
}

func NewDriver(config dbmate.DriverConfig) dbmate.Driver {
return &Driver{
migrationsTableName: config.MigrationsTableName,
databaseURL: config.DatabaseURL,
log: config.Log,
}
}

func connectionString(u *url.URL) string {
newURL := *u
newURL.Scheme = "libsql"
return newURL.String()
}

func (drv *Driver) CreateDatabase() error {
return fmt.Errorf("Please use turso-cli to create database")
}

func (drv *Driver) DropDatabase() error {
return fmt.Errorf("Please use turso-cli to drop database")
}

func (drv *Driver) DatabaseExists() (bool, error) {
db, err := drv.Open()
if err != nil {
return false, err
}
_, err = db.Exec("SELECT 1")
if err != nil {
return false, err
}
return true, nil
}

func (drv *Driver) Ping() error {
db, err := drv.Open()
if err != nil {
return err
}
defer dbutil.MustClose(db)
return db.Ping()
}

func (drv *Driver) Open() (*sql.DB, error) {
return sql.Open("libsql", connectionString(drv.databaseURL))
}

func (drv *Driver) MigrationsTableExists(db *sql.DB) (bool, error) {
exists := false
query := "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?"
err := db.QueryRow(query, drv.migrationsTableName).Scan(&exists)
if err == sql.ErrNoRows {
return false, nil
}
return exists, err
}

func (drv *Driver) CreateMigrationsTable(db *sql.DB) error {
query := fmt.Sprintf(
"CREATE TABLE IF NOT EXISTS %s (version TEXT PRIMARY KEY)",
drv.migrationsTableName,
)
_, err := db.Exec(query)
return err
}

func (drv *Driver) SelectMigrations(db *sql.DB, limit int) (map[string]bool, error) {
query := fmt.Sprintf("SELECT version FROM %s ORDER BY version DESC", drv.migrationsTableName)
if limit >= 0 {
query = fmt.Sprintf("%s LIMIT %d", query, limit)
}
rows, err := db.Query(query)
if err != nil {
return nil, err
}

defer dbutil.MustClose(rows)

migrations := map[string]bool{}
for rows.Next() {
var version string
if err := rows.Scan(&version); err != nil {
return nil, err
}

migrations[version] = true
}

if err = rows.Err(); err != nil {
return nil, err
}

return migrations, nil
}

func (drv *Driver) DeleteMigration(db dbutil.Transaction, version string) error {
query := fmt.Sprintf("DELETE FROM %s WHERE version = ?", drv.migrationsTableName)
_, err := db.Exec(query, version)
return err
}

func (drv *Driver) InsertMigration(db dbutil.Transaction, version string) error {
query := fmt.Sprintf("INSERT INTO %s (version) VALUES (?)", drv.migrationsTableName)
_, err := db.Exec(query, version)
return err
}

func (drv *Driver) DumpSchema(db *sql.DB) ([]byte, error) {
path := connectionString(drv.databaseURL)
schema, err := dbutil.RunCommand("turso", "db", "shell", path, ".schema")
if err != nil {
return nil, err
}

migrations, err := drv.schemaMigrationsDump(db)
if err != nil {
return nil, err
}

schema = append(schema, migrations...)
return dbutil.TrimLeadingSQLComments(schema)
}

func (drv *Driver) schemaMigrationsDump(db *sql.DB) ([]byte, error) {
// load applied migrations
query := fmt.Sprintf(
"SELECT quote(version) FROM %s order by version asc",
drv.migrationsTableName,
)
migrations, err := dbutil.QueryColumn(db, query)
if err != nil {
return nil, err
}

// build schema migrations table data
var buf bytes.Buffer
buf.WriteString("-- Dbmate schema migrations\n")
if len(migrations) > 0 {
buf.WriteString(
fmt.Sprintf("INSERT INTO %s (version) VALUES\n (", drv.migrationsTableName) +
strings.Join(migrations, "),\n (") +
");\n")
}

return buf.Bytes(), nil
}

func (drv *Driver) QueryError(query string, err error) error {
return &dbmate.QueryError{Err: err, Query: query}
}
15 changes: 15 additions & 0 deletions pkg/driver/turso/turso_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package turso

import (
"testing"

"github.com/amacneil/dbmate/v2/pkg/dbtest"
"github.com/stretchr/testify/require"
)

func TestConnectionString(t *testing.T) {
t.Run("substitutes schema", func(t *testing.T) {
u := dbtest.MustParseURL(t, "turso://example-database-dbmate.turso.io?authToken=fakeToken")
require.Equal(t, "libsql://example-database-dbmate.turso.io?authToken=fakeToken", connectionString(u))
})
}
Loading