feat(sql-migrate): add sqlite and sqlcmd --sql-command aliases

- Use full flag names in command defaults (--no-align, --tuples-only, etc.)
- Add sqlite/sqlite3/lite alias for SQLite
- Add sqlcmd/mssql/sqlserver alias for SQL Server (go-sqlcmd, TDS 8.0)
- Add SQL Server help text with SQLCMD env vars and TLS docs
- Add sqlite and sqlcmd SELECT expressions for id+name migration log
This commit is contained in:
AJ ONeal 2026-04-09 05:40:36 -06:00
parent 86ff126df0
commit 2256b12223
No known key found for this signature in database

View File

@ -43,9 +43,11 @@ var (
const ( const (
defaultMigrationDir = "./sql/migrations/" defaultMigrationDir = "./sql/migrations/"
defaultLogPath = "../migrations.log" defaultLogPath = "../migrations.log"
sqlCommandPSQL = `psql "$PG_URL" -v ON_ERROR_STOP=on -A -t --file %s` sqlCommandPSQL = `psql "$PG_URL" -v ON_ERROR_STOP=on --no-align --tuples-only --file %s`
sqlCommandMariaDB = `mariadb --defaults-extra-file="$MY_CNF" -s -N --raw < %s` sqlCommandMariaDB = `mariadb --defaults-extra-file="$MY_CNF" --silent --skip-column-names --raw < %s`
sqlCommandMySQL = `mysql --defaults-extra-file="$MY_CNF" -s -N --raw < %s` sqlCommandMySQL = `mysql --defaults-extra-file="$MY_CNF" --silent --skip-column-names --raw < %s`
sqlCommandSQLite = `sqlite3 "$SQLITE_PATH" < %s`
sqlCommandSQLCmd = `sqlcmd --exit-on-error --headers -1 --trim-spaces --encrypt-connection strict --input-file %s`
LOG_QUERY_NAME = "_migrations.sql" LOG_QUERY_NAME = "_migrations.sql"
M_MIGRATOR_NAME = "0001-01-01-001000_init-migrations" M_MIGRATOR_NAME = "0001-01-01-001000_init-migrations"
M_MIGRATOR_UP_NAME = "0001-01-01-001000_init-migrations.up.sql" M_MIGRATOR_UP_NAME = "0001-01-01-001000_init-migrations.up.sql"
@ -73,6 +75,7 @@ DROP TABLE IF EXISTS _migrations;
logMigrationsQueryPrev2_2_0 = `SELECT name FROM _migrations ORDER BY name;` logMigrationsQueryPrev2_2_0 = `SELECT name FROM _migrations ORDER BY name;`
logMigrationsQueryNote = "-- note: CLI arguments must be passed to the sql command to keep output machine-readable\n" logMigrationsQueryNote = "-- note: CLI arguments must be passed to the sql command to keep output machine-readable\n"
logMigrationsQuerySQLCmdNote = "-- connection: set SQLCMDSERVER, SQLCMDDATABASE, SQLCMDUSER, SQLCMDPASSWORD in .env\n"
) )
// printVersion displays the version, commit, and build date. // printVersion displays the version, commit, and build date.
@ -87,7 +90,7 @@ USAGE
sql-migrate [-d sqldir] <command> [args] sql-migrate [-d sqldir] <command> [args]
EXAMPLE EXAMPLE
sql-migrate -d ./sql/migrations/ init --sql-command <psql|mariadb|mysql> sql-migrate -d ./sql/migrations/ init --sql-command <psql|mariadb|mysql|sqlite|sqlcmd>
sql-migrate -d ./sql/migrations/ create <kebab-case-description> sql-migrate -d ./sql/migrations/ create <kebab-case-description>
sql-migrate -d ./sql/migrations/ sync sql-migrate -d ./sql/migrations/ sync
sql-migrate -d ./sql/migrations/ status sql-migrate -d ./sql/migrations/ status
@ -138,6 +141,31 @@ NOTE: POSTGRES SCHEMAS
Each schema gets its own _migrations table, so tenants are migrated Each schema gets its own _migrations table, so tenants are migrated
independently. PGOPTIONS is supported by psql and all libpq clients. independently. PGOPTIONS is supported by psql and all libpq clients.
NOTE: SQL SERVER (go-sqlcmd)
Requires the modern sqlcmd (go-mssqldb), not the legacy ODBC version.
Install: brew install sqlcmd (macOS), winget install sqlcmd (Windows)
The default uses --encrypt-connection strict (TDS 8.0), which provides
TLS-first on TCP with ALPN 'tds/8.0' and SNI required for proper TLS
termination at load balancers and reverse proxies.
Set these SQLCMD environment variables in your .env file:
SQLCMDSERVER='host\instance' # or host,port (e.g. localhost,1433)
SQLCMDDATABASE=myapp
SQLCMDUSER=sa
SQLCMDPASSWORD=secret
SQLCMDSERVER is the instance, not just the host. Common formats:
SQLCMDSERVER=localhost # default instance
SQLCMDSERVER='localhost\SQLEXPRESS' # named instance (quote the backslash)
SQLCMDSERVER='localhost,1433' # host and port
sqlcmd reads these automatically no credentials in the command template.
For local development without TLS:
--sql-command 'sqlcmd --exit-on-error --headers -1 --trim-spaces --encrypt-connection disable --input-file %s'
UPGRADING UPGRADING
After upgrading sql-migrate, run sync to refresh the log format: After upgrading sql-migrate, run sync to refresh the log format:
sql-migrate -d ./sql/migrations/ sync | sh sql-migrate -d ./sql/migrations/ sync | sh
@ -157,6 +185,10 @@ func logMigrationsSelect(sqlCommand string) string {
selectExpr = "id || CHR(9) || name" selectExpr = "id || CHR(9) || name"
case strings.Contains(sqlCommand, "mysql") || strings.Contains(sqlCommand, "mariadb"): case strings.Contains(sqlCommand, "mysql") || strings.Contains(sqlCommand, "mariadb"):
selectExpr = "CONCAT(id, CHAR(9), name)" selectExpr = "CONCAT(id, CHAR(9), name)"
case strings.Contains(sqlCommand, "sqlite"):
selectExpr = "id || CHAR(9) || name"
case strings.Contains(sqlCommand, "sqlcmd"):
selectExpr = "id + CHAR(9) + name"
default: default:
fmt.Fprintf(os.Stderr, "Error: unrecognized --sql-command %q; cannot generate _migrations.sql\n", sqlCommand) fmt.Fprintf(os.Stderr, "Error: unrecognized --sql-command %q; cannot generate _migrations.sql\n", sqlCommand)
os.Exit(1) os.Exit(1)
@ -225,7 +257,7 @@ func main() {
case "init": case "init":
fsSub = flag.NewFlagSet("init", flag.ExitOnError) fsSub = flag.NewFlagSet("init", flag.ExitOnError)
fsSub.StringVar(&cfg.logPath, "migrations-log", "", fmt.Sprintf("migration log file (default: %s) relative to and saved in %s", defaultLogPath, M_MIGRATOR_NAME)) fsSub.StringVar(&cfg.logPath, "migrations-log", "", fmt.Sprintf("migration log file (default: %s) relative to and saved in %s", defaultLogPath, M_MIGRATOR_NAME))
fsSub.StringVar(&cfg.sqlCommand, "sql-command", sqlCommandPSQL, "construct scripts with this to execute SQL files: 'psql', 'mysql', 'mariadb', or custom arguments") fsSub.StringVar(&cfg.sqlCommand, "sql-command", sqlCommandPSQL, "construct scripts with this to execute SQL files: 'psql', 'mysql', 'mariadb', 'sqlite', 'sqlcmd', or custom arguments")
case "create", "sync", "up", "down", "status", "list": case "create", "sync", "up", "down", "status", "list":
fsSub = flag.NewFlagSet(subcmd, flag.ExitOnError) fsSub = flag.NewFlagSet(subcmd, flag.ExitOnError)
default: default:
@ -246,6 +278,10 @@ func main() {
cfg.sqlCommand = sqlCommandMariaDB cfg.sqlCommand = sqlCommandMariaDB
case "mysql", "my": case "mysql", "my":
cfg.sqlCommand = sqlCommandMySQL cfg.sqlCommand = sqlCommandMySQL
case "sqlite", "sqlite3", "lite":
cfg.sqlCommand = sqlCommandSQLite
case "sqlcmd", "mssql", "sqlserver":
cfg.sqlCommand = sqlCommandSQLCmd
default: default:
// leave as provided by the user // leave as provided by the user
} }
@ -587,7 +623,11 @@ func mustInit(cfg *MainConfig) {
} }
logQueryPath := filepath.Join(state.MigrationsDir, LOG_QUERY_NAME) logQueryPath := filepath.Join(state.MigrationsDir, LOG_QUERY_NAME)
if created, err := initFile(logQueryPath, logMigrationsQueryNote+logMigrationsSelect(state.SQLCommand)+"\n"); err != nil { queryHeader := logMigrationsQueryNote
if strings.Contains(state.SQLCommand, "sqlcmd") {
queryHeader += logMigrationsQuerySQLCmdNote
}
if created, err := initFile(logQueryPath, queryHeader+logMigrationsSelect(state.SQLCommand)+"\n"); err != nil {
fmt.Fprintf(os.Stderr, "Error: init couldn't create migrations query: %v\n", err) fmt.Fprintf(os.Stderr, "Error: init couldn't create migrations query: %v\n", err)
os.Exit(1) os.Exit(1)
} else if created { } else if created {