Initial lookbook implementation
Pinterest-style visual bookmarking app with: - URL metadata extraction (OG/Twitter meta, oEmbed fallback) - Image caching in Postgres with 480px thumbnails - Multi-tag filtering with Ctrl/Cmd for OR mode - Fuzzy tag suggestions and inline tag editing - Browser console auth() with first-use password setup - Brutalist UI with Commit Mono font and Pico CSS - Light/dark mode via browser preference
This commit is contained in:
commit
fc625fb9cf
486 changed files with 195373 additions and 0 deletions
37
vendor/github.com/pressly/goose/v3/internal/controller/store.go
generated
vendored
Normal file
37
vendor/github.com/pressly/goose/v3/internal/controller/store.go
generated
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/pressly/goose/v3/database"
|
||||
)
|
||||
|
||||
// A StoreController is used by the goose package to interact with a database. This type is a
|
||||
// wrapper around the Store interface, but can be extended to include additional (optional) methods
|
||||
// that are not part of the core Store interface.
|
||||
type StoreController struct{ database.Store }
|
||||
|
||||
var _ database.StoreExtender = (*StoreController)(nil)
|
||||
|
||||
// NewStoreController returns a new StoreController that wraps the given Store.
|
||||
//
|
||||
// If the Store implements the following optional methods, the StoreController will call them as
|
||||
// appropriate:
|
||||
//
|
||||
// - TableExists(context.Context, DBTxConn) (bool, error)
|
||||
//
|
||||
// If the Store does not implement a method, it will either return a [errors.ErrUnsupported] error
|
||||
// or fall back to the default behavior.
|
||||
func NewStoreController(store database.Store) *StoreController {
|
||||
return &StoreController{store}
|
||||
}
|
||||
|
||||
func (c *StoreController) TableExists(ctx context.Context, db database.DBTxConn) (bool, error) {
|
||||
if t, ok := c.Store.(interface {
|
||||
TableExists(ctx context.Context, db database.DBTxConn) (bool, error)
|
||||
}); ok {
|
||||
return t.TableExists(ctx, db)
|
||||
}
|
||||
return false, errors.ErrUnsupported
|
||||
}
|
||||
53
vendor/github.com/pressly/goose/v3/internal/dialects/clickhouse.go
generated
vendored
Normal file
53
vendor/github.com/pressly/goose/v3/internal/dialects/clickhouse.go
generated
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package dialects
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pressly/goose/v3/database/dialect"
|
||||
)
|
||||
|
||||
// NewClickhouse returns a new [dialect.Querier] for Clickhouse dialect.
|
||||
func NewClickhouse() dialect.Querier {
|
||||
return &clickhouse{}
|
||||
}
|
||||
|
||||
type clickhouse struct{}
|
||||
|
||||
var _ dialect.Querier = (*clickhouse)(nil)
|
||||
|
||||
func (c *clickhouse) CreateTable(tableName string) string {
|
||||
q := `CREATE TABLE IF NOT EXISTS %s (
|
||||
version_id Int64,
|
||||
is_applied UInt8,
|
||||
date Date default now(),
|
||||
tstamp DateTime default now()
|
||||
)
|
||||
ENGINE = MergeTree()
|
||||
ORDER BY (date)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (c *clickhouse) InsertVersion(tableName string) string {
|
||||
q := `INSERT INTO %s (version_id, is_applied) VALUES ($1, $2)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (c *clickhouse) DeleteVersion(tableName string) string {
|
||||
q := `ALTER TABLE %s DELETE WHERE version_id = $1 SETTINGS mutations_sync = 2`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (c *clickhouse) GetMigrationByVersion(tableName string) string {
|
||||
q := `SELECT tstamp, is_applied FROM %s WHERE version_id = $1 ORDER BY tstamp DESC LIMIT 1`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (c *clickhouse) ListMigrations(tableName string) string {
|
||||
q := `SELECT version_id, is_applied FROM %s ORDER BY version_id DESC`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (c *clickhouse) GetLatestVersion(tableName string) string {
|
||||
q := `SELECT max(version_id) FROM %s`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
66
vendor/github.com/pressly/goose/v3/internal/dialects/dsql.go
generated
vendored
Normal file
66
vendor/github.com/pressly/goose/v3/internal/dialects/dsql.go
generated
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package dialects
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pressly/goose/v3/database/dialect"
|
||||
)
|
||||
|
||||
// NewAuroraDSQL returns a new [dialect.Querier] for Aurora DSQL dialect.
|
||||
func NewAuroraDSQL() dialect.QuerierExtender {
|
||||
return &dsql{}
|
||||
}
|
||||
|
||||
type dsql struct{}
|
||||
|
||||
var _ dialect.QuerierExtender = (*dsql)(nil)
|
||||
|
||||
func (d *dsql) CreateTable(tableName string) string {
|
||||
q := `CREATE TABLE %s (
|
||||
id integer PRIMARY KEY,
|
||||
version_id bigint NOT NULL,
|
||||
is_applied boolean NOT NULL,
|
||||
tstamp timestamp NOT NULL DEFAULT now()
|
||||
)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (d *dsql) InsertVersion(tableName string) string {
|
||||
q := `INSERT INTO %s (id, version_id, is_applied)
|
||||
VALUES (
|
||||
COALESCE((SELECT MAX(id) FROM %s), 0) + 1,
|
||||
$1,
|
||||
$2
|
||||
)`
|
||||
return fmt.Sprintf(q, tableName, tableName)
|
||||
}
|
||||
|
||||
func (d *dsql) DeleteVersion(tableName string) string {
|
||||
q := `DELETE FROM %s WHERE version_id=$1`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (d *dsql) GetMigrationByVersion(tableName string) string {
|
||||
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=$1 ORDER BY tstamp DESC LIMIT 1`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (d *dsql) ListMigrations(tableName string) string {
|
||||
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (d *dsql) GetLatestVersion(tableName string) string {
|
||||
q := `SELECT max(version_id) FROM %s`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (d *dsql) TableExists(tableName string) string {
|
||||
schemaName, tableName := parseTableIdentifier(tableName)
|
||||
if schemaName != "" {
|
||||
q := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE schemaname = '%s' AND tablename = '%s' )`
|
||||
return fmt.Sprintf(q, schemaName, tableName)
|
||||
}
|
||||
q := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE (current_schema() IS NULL OR schemaname = current_schema()) AND tablename = '%s' )`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
62
vendor/github.com/pressly/goose/v3/internal/dialects/mysql.go
generated
vendored
Normal file
62
vendor/github.com/pressly/goose/v3/internal/dialects/mysql.go
generated
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package dialects
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pressly/goose/v3/database/dialect"
|
||||
)
|
||||
|
||||
// NewMysql returns a new [dialect.Querier] for MySQL dialect.
|
||||
func NewMysql() dialect.QuerierExtender {
|
||||
return &mysql{}
|
||||
}
|
||||
|
||||
type mysql struct{}
|
||||
|
||||
var _ dialect.QuerierExtender = (*mysql)(nil)
|
||||
|
||||
func (m *mysql) CreateTable(tableName string) string {
|
||||
q := `CREATE TABLE %s (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
version_id bigint NOT NULL,
|
||||
is_applied boolean NOT NULL,
|
||||
tstamp timestamp NULL default now(),
|
||||
PRIMARY KEY(id)
|
||||
)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (m *mysql) InsertVersion(tableName string) string {
|
||||
q := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (m *mysql) DeleteVersion(tableName string) string {
|
||||
q := `DELETE FROM %s WHERE version_id=?`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (m *mysql) GetMigrationByVersion(tableName string) string {
|
||||
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (m *mysql) ListMigrations(tableName string) string {
|
||||
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (m *mysql) GetLatestVersion(tableName string) string {
|
||||
q := `SELECT MAX(version_id) FROM %s`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (m *mysql) TableExists(tableName string) string {
|
||||
schemaName, tableName := parseTableIdentifier(tableName)
|
||||
if schemaName != "" {
|
||||
q := `SELECT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = '%s' AND table_name = '%s' )`
|
||||
return fmt.Sprintf(q, schemaName, tableName)
|
||||
}
|
||||
q := `SELECT EXISTS ( SELECT 1 FROM information_schema.tables WHERE (database() IS NULL OR table_schema = database()) AND table_name = '%s' )`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
70
vendor/github.com/pressly/goose/v3/internal/dialects/postgres.go
generated
vendored
Normal file
70
vendor/github.com/pressly/goose/v3/internal/dialects/postgres.go
generated
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package dialects
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pressly/goose/v3/database/dialect"
|
||||
)
|
||||
|
||||
// NewPostgres returns a new [dialect.Querier] for PostgreSQL dialect.
|
||||
func NewPostgres() dialect.QuerierExtender {
|
||||
return &postgres{}
|
||||
}
|
||||
|
||||
type postgres struct{}
|
||||
|
||||
var _ dialect.QuerierExtender = (*postgres)(nil)
|
||||
|
||||
func (p *postgres) CreateTable(tableName string) string {
|
||||
q := `CREATE TABLE %s (
|
||||
id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
|
||||
version_id bigint NOT NULL,
|
||||
is_applied boolean NOT NULL,
|
||||
tstamp timestamp NOT NULL DEFAULT now()
|
||||
)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (p *postgres) InsertVersion(tableName string) string {
|
||||
q := `INSERT INTO %s (version_id, is_applied) VALUES ($1, $2)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (p *postgres) DeleteVersion(tableName string) string {
|
||||
q := `DELETE FROM %s WHERE version_id=$1`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (p *postgres) GetMigrationByVersion(tableName string) string {
|
||||
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=$1 ORDER BY tstamp DESC LIMIT 1`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (p *postgres) ListMigrations(tableName string) string {
|
||||
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (p *postgres) GetLatestVersion(tableName string) string {
|
||||
q := `SELECT max(version_id) FROM %s`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (p *postgres) TableExists(tableName string) string {
|
||||
schemaName, tableName := parseTableIdentifier(tableName)
|
||||
if schemaName != "" {
|
||||
q := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE schemaname = '%s' AND tablename = '%s' )`
|
||||
return fmt.Sprintf(q, schemaName, tableName)
|
||||
}
|
||||
q := `SELECT EXISTS ( SELECT 1 FROM pg_tables WHERE (current_schema() IS NULL OR schemaname = current_schema()) AND tablename = '%s' )`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func parseTableIdentifier(name string) (schema, table string) {
|
||||
schema, table, found := strings.Cut(name, ".")
|
||||
if !found {
|
||||
return "", name
|
||||
}
|
||||
return schema, table
|
||||
}
|
||||
52
vendor/github.com/pressly/goose/v3/internal/dialects/redshift.go
generated
vendored
Normal file
52
vendor/github.com/pressly/goose/v3/internal/dialects/redshift.go
generated
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package dialects
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pressly/goose/v3/database/dialect"
|
||||
)
|
||||
|
||||
// Redshift returns a new [dialect.Querier] for Redshift dialect.
|
||||
func NewRedshift() dialect.Querier {
|
||||
return &redshift{}
|
||||
}
|
||||
|
||||
type redshift struct{}
|
||||
|
||||
var _ dialect.Querier = (*redshift)(nil)
|
||||
|
||||
func (r *redshift) CreateTable(tableName string) string {
|
||||
q := `CREATE TABLE %s (
|
||||
id integer NOT NULL identity(1, 1),
|
||||
version_id bigint NOT NULL,
|
||||
is_applied boolean NOT NULL,
|
||||
tstamp timestamp NULL default sysdate,
|
||||
PRIMARY KEY(id)
|
||||
)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (r *redshift) InsertVersion(tableName string) string {
|
||||
q := `INSERT INTO %s (version_id, is_applied) VALUES ($1, $2)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (r *redshift) DeleteVersion(tableName string) string {
|
||||
q := `DELETE FROM %s WHERE version_id=$1`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (r *redshift) GetMigrationByVersion(tableName string) string {
|
||||
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=$1 ORDER BY tstamp DESC LIMIT 1`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (r *redshift) ListMigrations(tableName string) string {
|
||||
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (r *redshift) GetLatestVersion(tableName string) string {
|
||||
q := `SELECT max(version_id) FROM %s`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
51
vendor/github.com/pressly/goose/v3/internal/dialects/sqlite3.go
generated
vendored
Normal file
51
vendor/github.com/pressly/goose/v3/internal/dialects/sqlite3.go
generated
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package dialects
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pressly/goose/v3/database/dialect"
|
||||
)
|
||||
|
||||
// NewSqlite3 returns a [dialect.Querier] for SQLite3 dialect.
|
||||
func NewSqlite3() dialect.Querier {
|
||||
return &sqlite3{}
|
||||
}
|
||||
|
||||
type sqlite3 struct{}
|
||||
|
||||
var _ dialect.Querier = (*sqlite3)(nil)
|
||||
|
||||
func (s *sqlite3) CreateTable(tableName string) string {
|
||||
q := `CREATE TABLE %s (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
version_id INTEGER NOT NULL,
|
||||
is_applied INTEGER NOT NULL,
|
||||
tstamp TIMESTAMP DEFAULT (datetime('now'))
|
||||
)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (s *sqlite3) InsertVersion(tableName string) string {
|
||||
q := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (s *sqlite3) DeleteVersion(tableName string) string {
|
||||
q := `DELETE FROM %s WHERE version_id=?`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (s *sqlite3) GetMigrationByVersion(tableName string) string {
|
||||
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (s *sqlite3) ListMigrations(tableName string) string {
|
||||
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (s *sqlite3) GetLatestVersion(tableName string) string {
|
||||
q := `SELECT MAX(version_id) FROM %s`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
51
vendor/github.com/pressly/goose/v3/internal/dialects/sqlserver.go
generated
vendored
Normal file
51
vendor/github.com/pressly/goose/v3/internal/dialects/sqlserver.go
generated
vendored
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package dialects
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pressly/goose/v3/database/dialect"
|
||||
)
|
||||
|
||||
// NewSqlserver returns a [dialect.Querier] for SQL Server dialect.
|
||||
func NewSqlserver() dialect.Querier {
|
||||
return &sqlserver{}
|
||||
}
|
||||
|
||||
type sqlserver struct{}
|
||||
|
||||
var _ dialect.Querier = (*sqlserver)(nil)
|
||||
|
||||
func (s *sqlserver) CreateTable(tableName string) string {
|
||||
q := `CREATE TABLE %s (
|
||||
id INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
|
||||
version_id BIGINT NOT NULL,
|
||||
is_applied BIT NOT NULL,
|
||||
tstamp DATETIME NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (s *sqlserver) InsertVersion(tableName string) string {
|
||||
q := `INSERT INTO %s (version_id, is_applied) VALUES (@p1, @p2)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (s *sqlserver) DeleteVersion(tableName string) string {
|
||||
q := `DELETE FROM %s WHERE version_id=@p1`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (s *sqlserver) GetMigrationByVersion(tableName string) string {
|
||||
q := `SELECT TOP 1 tstamp, is_applied FROM %s WHERE version_id=@p1 ORDER BY tstamp DESC`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (s *sqlserver) ListMigrations(tableName string) string {
|
||||
q := `SELECT version_id, is_applied FROM %s ORDER BY id DESC`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (s *sqlserver) GetLatestVersion(tableName string) string {
|
||||
q := `SELECT MAX(version_id) FROM %s`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
54
vendor/github.com/pressly/goose/v3/internal/dialects/starrocks.go
generated
vendored
Normal file
54
vendor/github.com/pressly/goose/v3/internal/dialects/starrocks.go
generated
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package dialects
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pressly/goose/v3/database/dialect"
|
||||
)
|
||||
|
||||
// NewStarrocks returns a [dialect.Querier] for StarRocks dialect.
|
||||
func NewStarrocks() dialect.Querier {
|
||||
return &starrocks{}
|
||||
}
|
||||
|
||||
type starrocks struct{}
|
||||
|
||||
var _ dialect.Querier = (*starrocks)(nil)
|
||||
|
||||
func (m *starrocks) CreateTable(tableName string) string {
|
||||
q := `CREATE TABLE IF NOT EXISTS %s (
|
||||
id bigint NOT NULL AUTO_INCREMENT,
|
||||
version_id bigint NOT NULL,
|
||||
is_applied boolean NOT NULL,
|
||||
tstamp datetime NULL default CURRENT_TIMESTAMP
|
||||
)
|
||||
PRIMARY KEY (id)
|
||||
DISTRIBUTED BY HASH (id)
|
||||
ORDER BY (id,version_id)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (m *starrocks) InsertVersion(tableName string) string {
|
||||
q := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (m *starrocks) DeleteVersion(tableName string) string {
|
||||
q := `DELETE FROM %s WHERE version_id=?`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (m *starrocks) GetMigrationByVersion(tableName string) string {
|
||||
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (m *starrocks) ListMigrations(tableName string) string {
|
||||
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (m *starrocks) GetLatestVersion(tableName string) string {
|
||||
q := `SELECT MAX(version_id) FROM %s`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
52
vendor/github.com/pressly/goose/v3/internal/dialects/tidb.go
generated
vendored
Normal file
52
vendor/github.com/pressly/goose/v3/internal/dialects/tidb.go
generated
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package dialects
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pressly/goose/v3/database/dialect"
|
||||
)
|
||||
|
||||
// NewTidb returns a [dialect.Querier] for TiDB dialect.
|
||||
func NewTidb() dialect.Querier {
|
||||
return &Tidb{}
|
||||
}
|
||||
|
||||
type Tidb struct{}
|
||||
|
||||
var _ dialect.Querier = (*Tidb)(nil)
|
||||
|
||||
func (t *Tidb) CreateTable(tableName string) string {
|
||||
q := `CREATE TABLE %s (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
|
||||
version_id bigint NOT NULL,
|
||||
is_applied boolean NOT NULL,
|
||||
tstamp timestamp NULL default now(),
|
||||
PRIMARY KEY(id)
|
||||
)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (t *Tidb) InsertVersion(tableName string) string {
|
||||
q := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (t *Tidb) DeleteVersion(tableName string) string {
|
||||
q := `DELETE FROM %s WHERE version_id=?`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (t *Tidb) GetMigrationByVersion(tableName string) string {
|
||||
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (t *Tidb) ListMigrations(tableName string) string {
|
||||
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (t *Tidb) GetLatestVersion(tableName string) string {
|
||||
q := `SELECT MAX(version_id) FROM %s`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
14
vendor/github.com/pressly/goose/v3/internal/dialects/turso.go
generated
vendored
Normal file
14
vendor/github.com/pressly/goose/v3/internal/dialects/turso.go
generated
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package dialects
|
||||
|
||||
import "github.com/pressly/goose/v3/database/dialect"
|
||||
|
||||
// NewTurso returns a [dialect.Querier] for Turso dialect.
|
||||
func NewTurso() dialect.Querier {
|
||||
return &turso{}
|
||||
}
|
||||
|
||||
type turso struct {
|
||||
sqlite3
|
||||
}
|
||||
|
||||
var _ dialect.Querier = (*turso)(nil)
|
||||
54
vendor/github.com/pressly/goose/v3/internal/dialects/vertica.go
generated
vendored
Normal file
54
vendor/github.com/pressly/goose/v3/internal/dialects/vertica.go
generated
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package dialects
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pressly/goose/v3/database/dialect"
|
||||
)
|
||||
|
||||
// NewVertica returns a new [dialect.Querier] for Vertica dialect.
|
||||
//
|
||||
// DEPRECATED: Vertica support is deprecated and will be removed in a future release.
|
||||
func NewVertica() dialect.Querier {
|
||||
return &vertica{}
|
||||
}
|
||||
|
||||
type vertica struct{}
|
||||
|
||||
var _ dialect.Querier = (*vertica)(nil)
|
||||
|
||||
func (v *vertica) CreateTable(tableName string) string {
|
||||
q := `CREATE TABLE %s (
|
||||
id identity(1,1) NOT NULL,
|
||||
version_id bigint NOT NULL,
|
||||
is_applied boolean NOT NULL,
|
||||
tstamp timestamp NULL default now(),
|
||||
PRIMARY KEY(id)
|
||||
)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (v *vertica) InsertVersion(tableName string) string {
|
||||
q := `INSERT INTO %s (version_id, is_applied) VALUES (?, ?)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (v *vertica) DeleteVersion(tableName string) string {
|
||||
q := `DELETE FROM %s WHERE version_id=?`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (v *vertica) GetMigrationByVersion(tableName string) string {
|
||||
q := `SELECT tstamp, is_applied FROM %s WHERE version_id=? ORDER BY tstamp DESC LIMIT 1`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (v *vertica) ListMigrations(tableName string) string {
|
||||
q := `SELECT version_id, is_applied from %s ORDER BY id DESC`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (v *vertica) GetLatestVersion(tableName string) string {
|
||||
q := `SELECT MAX(version_id) FROM %s`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
62
vendor/github.com/pressly/goose/v3/internal/dialects/ydb.go
generated
vendored
Normal file
62
vendor/github.com/pressly/goose/v3/internal/dialects/ydb.go
generated
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package dialects
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pressly/goose/v3/database/dialect"
|
||||
)
|
||||
|
||||
// NewYDB returns a new [dialect.Querier] for Vertica dialect.
|
||||
func NewYDB() dialect.Querier {
|
||||
return &ydb{}
|
||||
}
|
||||
|
||||
type ydb struct{}
|
||||
|
||||
var _ dialect.Querier = (*ydb)(nil)
|
||||
|
||||
func (c *ydb) CreateTable(tableName string) string {
|
||||
q := `CREATE TABLE %s (
|
||||
version_id Uint64,
|
||||
is_applied Bool,
|
||||
tstamp Timestamp,
|
||||
|
||||
PRIMARY KEY(version_id)
|
||||
)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (c *ydb) InsertVersion(tableName string) string {
|
||||
q := `INSERT INTO %s (
|
||||
version_id,
|
||||
is_applied,
|
||||
tstamp
|
||||
) VALUES (
|
||||
CAST($1 AS Uint64),
|
||||
$2,
|
||||
CurrentUtcTimestamp()
|
||||
)`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (c *ydb) DeleteVersion(tableName string) string {
|
||||
q := `DELETE FROM %s WHERE version_id = $1`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (c *ydb) GetMigrationByVersion(tableName string) string {
|
||||
q := `SELECT tstamp, is_applied FROM %s WHERE version_id = $1 ORDER BY tstamp DESC LIMIT 1`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (c *ydb) ListMigrations(tableName string) string {
|
||||
q := `
|
||||
SELECT version_id, is_applied, tstamp AS __discard_column_tstamp
|
||||
FROM %s ORDER BY __discard_column_tstamp DESC`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
|
||||
func (c *ydb) GetLatestVersion(tableName string) string {
|
||||
q := `SELECT MAX(version_id) FROM %s`
|
||||
return fmt.Sprintf(q, tableName)
|
||||
}
|
||||
124
vendor/github.com/pressly/goose/v3/internal/gooseutil/resolve.go
generated
vendored
Normal file
124
vendor/github.com/pressly/goose/v3/internal/gooseutil/resolve.go
generated
vendored
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// Package gooseutil provides utility functions we want to keep internal to the package. It's
|
||||
// intended to be a collection of well-tested helper functions.
|
||||
package gooseutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UpVersions returns a list of migrations to apply based on the versions in the filesystem and the
|
||||
// versions in the database. The target version can be used to specify a target version. In most
|
||||
// cases this will be math.MaxInt64.
|
||||
//
|
||||
// The allowMissing flag can be used to allow missing migrations as part of the list of migrations
|
||||
// to apply. Otherwise, an error will be returned if there are missing migrations in the database.
|
||||
func UpVersions(
|
||||
fsysVersions []int64,
|
||||
dbVersions []int64,
|
||||
target int64,
|
||||
allowMissing bool,
|
||||
) ([]int64, error) {
|
||||
// Sort the list of versions in the filesystem. This should already be sorted, but we do this
|
||||
// just in case.
|
||||
sortAscending(fsysVersions)
|
||||
|
||||
// dbAppliedVersions is a map of all applied migrations in the database.
|
||||
dbAppliedVersions := make(map[int64]bool, len(dbVersions))
|
||||
var dbMaxVersion int64
|
||||
for _, v := range dbVersions {
|
||||
dbAppliedVersions[v] = true
|
||||
if v > dbMaxVersion {
|
||||
dbMaxVersion = v
|
||||
}
|
||||
}
|
||||
|
||||
// Get a list of migrations that are missing from the database. A missing migration is one that
|
||||
// has a version less than the max version in the database and has not been applied.
|
||||
//
|
||||
// In most cases the target version is math.MaxInt64, but it can be used to specify a target
|
||||
// version. In which case we respect the target version and only surface migrations up to and
|
||||
// including that target.
|
||||
var missing []int64
|
||||
for _, v := range fsysVersions {
|
||||
if dbAppliedVersions[v] {
|
||||
continue
|
||||
}
|
||||
if v < dbMaxVersion && v <= target {
|
||||
missing = append(missing, v)
|
||||
}
|
||||
}
|
||||
|
||||
// feat(mf): It is very possible someone may want to apply ONLY new migrations and skip missing
|
||||
// migrations entirely. At the moment this is not supported, but leaving this comment because
|
||||
// that's where that logic would be handled.
|
||||
//
|
||||
// For example, if database has 1,4 already applied and 2,3,5 are new, we would apply only 5 and
|
||||
// skip 2,3. Not sure if this is a common use case, but it's possible someone may want to do
|
||||
// this.
|
||||
if len(missing) > 0 && !allowMissing {
|
||||
return nil, newMissingError(missing, dbMaxVersion, target)
|
||||
}
|
||||
|
||||
var out []int64
|
||||
|
||||
// 1. Add missing migrations to the list of migrations to apply, if any.
|
||||
out = append(out, missing...)
|
||||
|
||||
// 2. Add new migrations to the list of migrations to apply, if any.
|
||||
for _, v := range fsysVersions {
|
||||
if dbAppliedVersions[v] {
|
||||
continue
|
||||
}
|
||||
if v > dbMaxVersion && v <= target {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
// 3. Sort the list of migrations to apply.
|
||||
sortAscending(out)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func newMissingError(
|
||||
missing []int64,
|
||||
dbMaxVersion int64,
|
||||
target int64,
|
||||
) error {
|
||||
sortAscending(missing)
|
||||
|
||||
collected := make([]string, 0, len(missing))
|
||||
for _, v := range missing {
|
||||
collected = append(collected, strconv.FormatInt(v, 10))
|
||||
}
|
||||
|
||||
msg := "migration"
|
||||
if len(collected) > 1 {
|
||||
msg += "s"
|
||||
}
|
||||
|
||||
var versionsMsg string
|
||||
if len(collected) > 1 {
|
||||
versionsMsg = "versions " + strings.Join(collected, ",")
|
||||
} else {
|
||||
versionsMsg = "version " + collected[0]
|
||||
}
|
||||
|
||||
desiredMsg := fmt.Sprintf("database version (%d)", dbMaxVersion)
|
||||
if target != math.MaxInt64 {
|
||||
desiredMsg += fmt.Sprintf(", with target version (%d)", target)
|
||||
}
|
||||
|
||||
return fmt.Errorf("detected %d missing (out-of-order) %s lower than %s: %s",
|
||||
len(missing), msg, desiredMsg, versionsMsg,
|
||||
)
|
||||
}
|
||||
|
||||
func sortAscending(versions []int64) {
|
||||
sort.Slice(versions, func(i, j int) bool {
|
||||
return versions[i] < versions[j]
|
||||
})
|
||||
}
|
||||
171
vendor/github.com/pressly/goose/v3/internal/legacystore/legacystore.go
generated
vendored
Normal file
171
vendor/github.com/pressly/goose/v3/internal/legacystore/legacystore.go
generated
vendored
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
package legacystore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pressly/goose/v3/database"
|
||||
"github.com/pressly/goose/v3/database/dialect"
|
||||
"github.com/pressly/goose/v3/internal/dialects"
|
||||
)
|
||||
|
||||
// Store is the interface that wraps the basic methods for a database dialect.
|
||||
//
|
||||
// A dialect is a set of SQL statements that are specific to a database.
|
||||
//
|
||||
// By defining a store interface, we can support multiple databases
|
||||
// with a single codebase.
|
||||
//
|
||||
// The underlying implementation does not modify the error. It is the callers
|
||||
// responsibility to assert for the correct error, such as sql.ErrNoRows.
|
||||
type Store interface {
|
||||
// CreateVersionTable creates the version table within a transaction.
|
||||
// This table is used to store goose migrations.
|
||||
CreateVersionTable(ctx context.Context, tx *sql.Tx, tableName string) error
|
||||
|
||||
// InsertVersion inserts a version id into the version table within a transaction.
|
||||
InsertVersion(ctx context.Context, tx *sql.Tx, tableName string, version int64) error
|
||||
// InsertVersionNoTx inserts a version id into the version table without a transaction.
|
||||
InsertVersionNoTx(ctx context.Context, db *sql.DB, tableName string, version int64) error
|
||||
|
||||
// DeleteVersion deletes a version id from the version table within a transaction.
|
||||
DeleteVersion(ctx context.Context, tx *sql.Tx, tableName string, version int64) error
|
||||
// DeleteVersionNoTx deletes a version id from the version table without a transaction.
|
||||
DeleteVersionNoTx(ctx context.Context, db *sql.DB, tableName string, version int64) error
|
||||
|
||||
// GetMigrationRow retrieves a single migration by version id.
|
||||
//
|
||||
// Returns the raw sql error if the query fails. It is the callers responsibility
|
||||
// to assert for the correct error, such as sql.ErrNoRows.
|
||||
GetMigration(ctx context.Context, db *sql.DB, tableName string, version int64) (*GetMigrationResult, error)
|
||||
|
||||
// ListMigrations retrieves all migrations sorted in descending order by id.
|
||||
//
|
||||
// If there are no migrations, an empty slice is returned with no error.
|
||||
ListMigrations(ctx context.Context, db *sql.DB, tableName string) ([]*ListMigrationsResult, error)
|
||||
}
|
||||
|
||||
// NewStore returns a new Store for the given dialect.
|
||||
func NewStore(d database.Dialect) (Store, error) {
|
||||
var querier dialect.Querier
|
||||
switch d {
|
||||
case database.DialectPostgres:
|
||||
querier = dialects.NewPostgres()
|
||||
case database.DialectMySQL:
|
||||
querier = dialects.NewMysql()
|
||||
case database.DialectSQLite3:
|
||||
querier = dialects.NewSqlite3()
|
||||
case database.DialectMSSQL:
|
||||
querier = dialects.NewSqlserver()
|
||||
case database.DialectRedshift:
|
||||
querier = dialects.NewRedshift()
|
||||
case database.DialectTiDB:
|
||||
querier = dialects.NewTidb()
|
||||
case database.DialectClickHouse:
|
||||
querier = dialects.NewClickhouse()
|
||||
case database.DialectVertica:
|
||||
querier = dialects.NewVertica()
|
||||
case database.DialectYdB:
|
||||
querier = dialects.NewYDB()
|
||||
case database.DialectTurso:
|
||||
querier = dialects.NewTurso()
|
||||
case database.DialectStarrocks:
|
||||
querier = dialects.NewStarrocks()
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown querier dialect: %v", d)
|
||||
}
|
||||
return &store{querier: querier}, nil
|
||||
}
|
||||
|
||||
type GetMigrationResult struct {
|
||||
IsApplied bool
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
type ListMigrationsResult struct {
|
||||
VersionID int64
|
||||
IsApplied bool
|
||||
}
|
||||
|
||||
type store struct {
|
||||
querier dialect.Querier
|
||||
}
|
||||
|
||||
var _ Store = (*store)(nil)
|
||||
|
||||
func (s *store) CreateVersionTable(ctx context.Context, tx *sql.Tx, tableName string) error {
|
||||
q := s.querier.CreateTable(tableName)
|
||||
_, err := tx.ExecContext(ctx, q)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *store) InsertVersion(ctx context.Context, tx *sql.Tx, tableName string, version int64) error {
|
||||
q := s.querier.InsertVersion(tableName)
|
||||
_, err := tx.ExecContext(ctx, q, version, true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *store) InsertVersionNoTx(ctx context.Context, db *sql.DB, tableName string, version int64) error {
|
||||
q := s.querier.InsertVersion(tableName)
|
||||
_, err := db.ExecContext(ctx, q, version, true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *store) DeleteVersion(ctx context.Context, tx *sql.Tx, tableName string, version int64) error {
|
||||
q := s.querier.DeleteVersion(tableName)
|
||||
_, err := tx.ExecContext(ctx, q, version)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *store) DeleteVersionNoTx(ctx context.Context, db *sql.DB, tableName string, version int64) error {
|
||||
q := s.querier.DeleteVersion(tableName)
|
||||
_, err := db.ExecContext(ctx, q, version)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *store) GetMigration(
|
||||
ctx context.Context,
|
||||
db *sql.DB,
|
||||
tableName string,
|
||||
version int64,
|
||||
) (*GetMigrationResult, error) {
|
||||
q := s.querier.GetMigrationByVersion(tableName)
|
||||
var timestamp time.Time
|
||||
var isApplied bool
|
||||
err := db.QueryRowContext(ctx, q, version).Scan(×tamp, &isApplied)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &GetMigrationResult{
|
||||
IsApplied: isApplied,
|
||||
Timestamp: timestamp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *store) ListMigrations(ctx context.Context, db *sql.DB, tableName string) ([]*ListMigrationsResult, error) {
|
||||
q := s.querier.ListMigrations(tableName)
|
||||
rows, err := db.QueryContext(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var migrations []*ListMigrationsResult
|
||||
for rows.Next() {
|
||||
var version int64
|
||||
var isApplied bool
|
||||
if err := rows.Scan(&version, &isApplied); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
migrations = append(migrations, &ListMigrationsResult{
|
||||
VersionID: version,
|
||||
IsApplied: isApplied,
|
||||
})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return migrations, nil
|
||||
}
|
||||
59
vendor/github.com/pressly/goose/v3/internal/sqlparser/parse.go
generated
vendored
Normal file
59
vendor/github.com/pressly/goose/v3/internal/sqlparser/parse.go
generated
vendored
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package sqlparser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
|
||||
"go.uber.org/multierr"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type ParsedSQL struct {
|
||||
UseTx bool
|
||||
Up, Down []string
|
||||
}
|
||||
|
||||
func ParseAllFromFS(fsys fs.FS, filename string, debug bool) (*ParsedSQL, error) {
|
||||
parsedSQL := new(ParsedSQL)
|
||||
// TODO(mf): parse is called twice, once for up and once for down. This is inefficient. It
|
||||
// should be possible to parse both directions in one pass. Also, UseTx is set once (but
|
||||
// returned twice), which is unnecessary and potentially error-prone if the two calls to
|
||||
// parseSQL disagree based on direction.
|
||||
var g errgroup.Group
|
||||
g.Go(func() error {
|
||||
up, useTx, err := parse(fsys, filename, DirectionUp, debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parsedSQL.Up = up
|
||||
parsedSQL.UseTx = useTx
|
||||
return nil
|
||||
})
|
||||
g.Go(func() error {
|
||||
down, _, err := parse(fsys, filename, DirectionDown, debug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parsedSQL.Down = down
|
||||
return nil
|
||||
})
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parsedSQL, nil
|
||||
}
|
||||
|
||||
func parse(fsys fs.FS, filename string, direction Direction, debug bool) (_ []string, _ bool, retErr error) {
|
||||
r, err := fsys.Open(filename)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer func() {
|
||||
retErr = multierr.Append(retErr, r.Close())
|
||||
}()
|
||||
stmts, useTx, err := ParseSQLMigration(r, direction, debug)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to parse %s: %w", filename, err)
|
||||
}
|
||||
return stmts, useTx, nil
|
||||
}
|
||||
394
vendor/github.com/pressly/goose/v3/internal/sqlparser/parser.go
generated
vendored
Normal file
394
vendor/github.com/pressly/goose/v3/internal/sqlparser/parser.go
generated
vendored
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
package sqlparser
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mfridman/interpolate"
|
||||
)
|
||||
|
||||
type Direction string
|
||||
|
||||
const (
|
||||
DirectionUp Direction = "up"
|
||||
DirectionDown Direction = "down"
|
||||
)
|
||||
|
||||
func FromBool(b bool) Direction {
|
||||
if b {
|
||||
return DirectionUp
|
||||
}
|
||||
return DirectionDown
|
||||
}
|
||||
|
||||
func (d Direction) String() string {
|
||||
return string(d)
|
||||
}
|
||||
|
||||
func (d Direction) ToBool() bool {
|
||||
return d == DirectionUp
|
||||
}
|
||||
|
||||
type parserState int
|
||||
|
||||
const (
|
||||
start parserState = iota // 0
|
||||
gooseUp // 1
|
||||
gooseStatementBeginUp // 2
|
||||
gooseStatementEndUp // 3
|
||||
gooseDown // 4
|
||||
gooseStatementBeginDown // 5
|
||||
gooseStatementEndDown // 6
|
||||
)
|
||||
|
||||
type stateMachine struct {
|
||||
state parserState
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func newStateMachine(begin parserState, verbose bool) *stateMachine {
|
||||
return &stateMachine{
|
||||
state: begin,
|
||||
verbose: verbose,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stateMachine) get() parserState {
|
||||
return s.state
|
||||
}
|
||||
|
||||
func (s *stateMachine) set(new parserState) {
|
||||
s.print("set %d => %d", s.state, new)
|
||||
s.state = new
|
||||
}
|
||||
|
||||
const (
|
||||
grayColor = "\033[90m"
|
||||
resetColor = "\033[00m"
|
||||
)
|
||||
|
||||
func (s *stateMachine) print(msg string, args ...interface{}) {
|
||||
msg = "StateMachine: " + msg
|
||||
if s.verbose {
|
||||
log.Printf(grayColor+msg+resetColor, args...)
|
||||
}
|
||||
}
|
||||
|
||||
const scanBufSize = 4 * 1024 * 1024
|
||||
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
buf := make([]byte, scanBufSize)
|
||||
return &buf
|
||||
},
|
||||
}
|
||||
|
||||
// Split given SQL script into individual statements and return
|
||||
// SQL statements for given direction (up=true, down=false).
|
||||
//
|
||||
// The base case is to simply split on semicolons, as these
|
||||
// naturally terminate a statement.
|
||||
//
|
||||
// However, more complex cases like pl/pgsql can have semicolons
|
||||
// within a statement. For these cases, we provide the explicit annotations
|
||||
// 'StatementBegin' and 'StatementEnd' to allow the script to
|
||||
// tell us to ignore semicolons.
|
||||
func ParseSQLMigration(r io.Reader, direction Direction, debug bool) (stmts []string, useTx bool, err error) {
|
||||
scanBufPtr := bufferPool.Get().(*[]byte)
|
||||
scanBuf := *scanBufPtr
|
||||
defer bufferPool.Put(scanBufPtr)
|
||||
|
||||
scanner := bufio.NewScanner(r)
|
||||
scanner.Buffer(scanBuf, scanBufSize)
|
||||
|
||||
stateMachine := newStateMachine(start, debug)
|
||||
useTx = true
|
||||
useEnvsub := false
|
||||
|
||||
var buf bytes.Buffer
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if debug {
|
||||
log.Println(line)
|
||||
}
|
||||
if stateMachine.get() == start && strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for annotations.
|
||||
// All annotations must be in format: "-- +goose [annotation]"
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "--") && strings.Contains(line, "+goose") {
|
||||
var cmd annotation
|
||||
|
||||
cmd, err = extractAnnotation(line)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to parse annotation line %q: %w", line, err)
|
||||
}
|
||||
|
||||
switch cmd {
|
||||
case annotationUp:
|
||||
switch stateMachine.get() {
|
||||
case start:
|
||||
stateMachine.set(gooseUp)
|
||||
default:
|
||||
return nil, false, fmt.Errorf("duplicate '-- +goose Up' annotations; stateMachine=%d, see https://github.com/pressly/goose#sql-migrations", stateMachine.state)
|
||||
}
|
||||
continue
|
||||
|
||||
case annotationDown:
|
||||
switch stateMachine.get() {
|
||||
case gooseUp, gooseStatementEndUp:
|
||||
// If we hit a down annotation, but the buffer is not empty, we have an unfinished SQL query from a
|
||||
// previous up annotation. This is an error, because we expect the SQL query to be terminated by a semicolon
|
||||
// and the buffer to have been reset.
|
||||
if bufferRemaining := strings.TrimSpace(buf.String()); len(bufferRemaining) > 0 {
|
||||
return nil, false, missingSemicolonError(stateMachine.state, direction, bufferRemaining)
|
||||
}
|
||||
stateMachine.set(gooseDown)
|
||||
default:
|
||||
return nil, false, fmt.Errorf("must start with '-- +goose Up' annotation, stateMachine=%d, see https://github.com/pressly/goose#sql-migrations", stateMachine.state)
|
||||
}
|
||||
continue
|
||||
|
||||
case annotationStatementBegin:
|
||||
switch stateMachine.get() {
|
||||
case gooseUp, gooseStatementEndUp:
|
||||
stateMachine.set(gooseStatementBeginUp)
|
||||
case gooseDown, gooseStatementEndDown:
|
||||
stateMachine.set(gooseStatementBeginDown)
|
||||
default:
|
||||
return nil, false, fmt.Errorf("'-- +goose StatementBegin' must be defined after '-- +goose Up' or '-- +goose Down' annotation, stateMachine=%d, see https://github.com/pressly/goose#sql-migrations", stateMachine.state)
|
||||
}
|
||||
continue
|
||||
|
||||
case annotationStatementEnd:
|
||||
switch stateMachine.get() {
|
||||
case gooseStatementBeginUp:
|
||||
stateMachine.set(gooseStatementEndUp)
|
||||
case gooseStatementBeginDown:
|
||||
stateMachine.set(gooseStatementEndDown)
|
||||
default:
|
||||
return nil, false, errors.New("'-- +goose StatementEnd' must be defined after '-- +goose StatementBegin', see https://github.com/pressly/goose#sql-migrations")
|
||||
}
|
||||
|
||||
case annotationNoTransaction:
|
||||
useTx = false
|
||||
continue
|
||||
|
||||
case annotationEnvsubOn:
|
||||
useEnvsub = true
|
||||
continue
|
||||
|
||||
case annotationEnvsubOff:
|
||||
useEnvsub = false
|
||||
continue
|
||||
|
||||
default:
|
||||
return nil, false, fmt.Errorf("unknown annotation: %q", cmd)
|
||||
}
|
||||
}
|
||||
// Once we've started parsing a statement the buffer is no longer empty,
|
||||
// we keep all comments up until the end of the statement (the buffer will be reset).
|
||||
// All other comments in the file are ignored.
|
||||
if buf.Len() == 0 {
|
||||
// This check ensures leading comments and empty lines prior to a statement are ignored.
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "--") || line == "" {
|
||||
stateMachine.print("ignore comment")
|
||||
continue
|
||||
}
|
||||
}
|
||||
switch stateMachine.get() {
|
||||
case gooseStatementEndDown, gooseStatementEndUp:
|
||||
// Do not include the "+goose StatementEnd" annotation in the final statement.
|
||||
default:
|
||||
if useEnvsub {
|
||||
expanded, err := interpolate.Interpolate(&envWrapper{}, line)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("variable substitution failed: %w:\n%s", err, line)
|
||||
}
|
||||
line = expanded
|
||||
}
|
||||
// Write SQL line to a buffer.
|
||||
if _, err := buf.WriteString(line + "\n"); err != nil {
|
||||
return nil, false, fmt.Errorf("failed to write to buf: %w", err)
|
||||
}
|
||||
}
|
||||
// Read SQL body one by line, if we're in the right direction.
|
||||
//
|
||||
// 1) basic query with semicolon; 2) psql statement
|
||||
//
|
||||
// Export statement once we hit end of statement.
|
||||
switch stateMachine.get() {
|
||||
case gooseUp, gooseStatementBeginUp, gooseStatementEndUp:
|
||||
if direction == DirectionDown {
|
||||
buf.Reset()
|
||||
stateMachine.print("ignore down")
|
||||
continue
|
||||
}
|
||||
case gooseDown, gooseStatementBeginDown, gooseStatementEndDown:
|
||||
if direction == DirectionUp {
|
||||
buf.Reset()
|
||||
stateMachine.print("ignore up")
|
||||
continue
|
||||
}
|
||||
default:
|
||||
return nil, false, fmt.Errorf("failed to parse migration: unexpected state %d on line %q, see https://github.com/pressly/goose#sql-migrations", stateMachine.state, line)
|
||||
}
|
||||
|
||||
switch stateMachine.get() {
|
||||
case gooseUp:
|
||||
if endsWithSemicolon(line) {
|
||||
stmts = append(stmts, cleanupStatement(buf.String()))
|
||||
buf.Reset()
|
||||
stateMachine.print("store simple Up query")
|
||||
}
|
||||
case gooseDown:
|
||||
if endsWithSemicolon(line) {
|
||||
stmts = append(stmts, cleanupStatement(buf.String()))
|
||||
buf.Reset()
|
||||
stateMachine.print("store simple Down query")
|
||||
}
|
||||
case gooseStatementEndUp:
|
||||
stmts = append(stmts, cleanupStatement(buf.String()))
|
||||
buf.Reset()
|
||||
stateMachine.print("store Up statement")
|
||||
stateMachine.set(gooseUp)
|
||||
case gooseStatementEndDown:
|
||||
stmts = append(stmts, cleanupStatement(buf.String()))
|
||||
buf.Reset()
|
||||
stateMachine.print("store Down statement")
|
||||
stateMachine.set(gooseDown)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, false, fmt.Errorf("failed to scan migration: %w", err)
|
||||
}
|
||||
// EOF
|
||||
|
||||
switch stateMachine.get() {
|
||||
case start:
|
||||
return nil, false, errors.New("failed to parse migration: must start with '-- +goose Up' annotation, see https://github.com/pressly/goose#sql-migrations")
|
||||
case gooseStatementBeginUp, gooseStatementBeginDown:
|
||||
return nil, false, errors.New("failed to parse migration: missing '-- +goose StatementEnd' annotation")
|
||||
}
|
||||
|
||||
if bufferRemaining := strings.TrimSpace(buf.String()); len(bufferRemaining) > 0 {
|
||||
return nil, false, missingSemicolonError(stateMachine.state, direction, bufferRemaining)
|
||||
}
|
||||
|
||||
return stmts, useTx, nil
|
||||
}
|
||||
|
||||
type annotation string
|
||||
|
||||
const (
|
||||
annotationUp annotation = "Up"
|
||||
annotationDown annotation = "Down"
|
||||
annotationStatementBegin annotation = "StatementBegin"
|
||||
annotationStatementEnd annotation = "StatementEnd"
|
||||
annotationNoTransaction annotation = "NO TRANSACTION"
|
||||
annotationEnvsubOn annotation = "ENVSUB ON"
|
||||
annotationEnvsubOff annotation = "ENVSUB OFF"
|
||||
)
|
||||
|
||||
var supportedAnnotations = map[annotation]struct{}{
|
||||
annotationUp: {},
|
||||
annotationDown: {},
|
||||
annotationStatementBegin: {},
|
||||
annotationStatementEnd: {},
|
||||
annotationNoTransaction: {},
|
||||
annotationEnvsubOn: {},
|
||||
annotationEnvsubOff: {},
|
||||
}
|
||||
|
||||
var (
|
||||
errEmptyAnnotation = errors.New("empty annotation")
|
||||
errInvalidAnnotation = errors.New("invalid annotation")
|
||||
)
|
||||
|
||||
// extractAnnotation extracts the annotation from the line.
|
||||
// All annotations must be in format: "-- +goose [annotation]"
|
||||
// Allowed annotations: Up, Down, StatementBegin, StatementEnd, NO TRANSACTION, ENVSUB ON, ENVSUB OFF
|
||||
func extractAnnotation(line string) (annotation, error) {
|
||||
// If line contains leading whitespace - return error.
|
||||
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") {
|
||||
return "", fmt.Errorf("%q contains leading whitespace: %w", line, errInvalidAnnotation)
|
||||
}
|
||||
|
||||
// Extract the annotation from the line, by removing the leading "--"
|
||||
cmd := strings.ReplaceAll(line, "--", "")
|
||||
|
||||
// Extract the annotation from the line, by removing the leading "+goose"
|
||||
cmd = strings.Replace(cmd, "+goose", "", 1)
|
||||
|
||||
if strings.Contains(cmd, "+goose") {
|
||||
return "", fmt.Errorf("%q contains multiple '+goose' annotations: %w", cmd, errInvalidAnnotation)
|
||||
}
|
||||
|
||||
// Remove leading and trailing whitespace from the annotation command.
|
||||
cmd = strings.TrimSpace(cmd)
|
||||
|
||||
if cmd == "" {
|
||||
return "", errEmptyAnnotation
|
||||
}
|
||||
|
||||
a := annotation(cmd)
|
||||
|
||||
for s := range supportedAnnotations {
|
||||
if strings.EqualFold(string(s), string(a)) {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%q not supported: %w", cmd, errInvalidAnnotation)
|
||||
}
|
||||
|
||||
func missingSemicolonError(state parserState, direction Direction, s string) error {
|
||||
return fmt.Errorf("failed to parse migration: state %d, direction: %v: unexpected unfinished SQL query: %q: missing semicolon?",
|
||||
state,
|
||||
direction,
|
||||
s,
|
||||
)
|
||||
}
|
||||
|
||||
type envWrapper struct{}
|
||||
|
||||
var _ interpolate.Env = (*envWrapper)(nil)
|
||||
|
||||
func (e *envWrapper) Get(key string) (string, bool) {
|
||||
return os.LookupEnv(key)
|
||||
}
|
||||
|
||||
func cleanupStatement(input string) string {
|
||||
return strings.TrimSpace(input)
|
||||
}
|
||||
|
||||
// Checks the line to see if the line has a statement-ending semicolon
|
||||
// or if the line contains a double-dash comment.
|
||||
func endsWithSemicolon(line string) bool {
|
||||
scanBufPtr := bufferPool.Get().(*[]byte)
|
||||
scanBuf := *scanBufPtr
|
||||
defer bufferPool.Put(scanBufPtr)
|
||||
|
||||
prev := ""
|
||||
scanner := bufio.NewScanner(strings.NewReader(line))
|
||||
scanner.Buffer(scanBuf, scanBufSize)
|
||||
scanner.Split(bufio.ScanWords)
|
||||
|
||||
for scanner.Scan() {
|
||||
word := scanner.Text()
|
||||
if strings.HasPrefix(word, "--") {
|
||||
break
|
||||
}
|
||||
prev = word
|
||||
}
|
||||
|
||||
return strings.HasSuffix(prev, ";")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue