feat(sql-migrate): add ID-in-log for tab-delimited migration tracking

- logMigrationsSelect() returns DB-specific SELECT for id+name output,
  matching psql explicitly and erroring on unrecognized --sql-command
- logMigrationsQueryNote const for the comment header
- maybeUpgradeLogQuery() auto-upgrades old _migrations.sql on first run,
  replacing only the matching SELECT line
- Add UPGRADING help section pointing to sync subcommand
This commit is contained in:
AJ ONeal 2026-04-08 17:23:50 -06:00
parent 3e51c7b67a
commit 55298ac018
No known key found for this signature in database

View File

@ -69,9 +69,10 @@ INSERT INTO _migrations (name, id) VALUES ('0001-01-01-001000_init-migrations',
DROP TABLE IF EXISTS _migrations;
`
LOG_MIGRATIONS_QUERY = `-- note: CLI arguments must be passed to the sql command to keep output clean
SELECT name FROM _migrations ORDER BY name;
`
// Used for detection during auto-upgrade.
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"
)
// printVersion displays the version, commit, and build date.
@ -100,6 +101,7 @@ COMMANDS
create - creates a new, canonically-named up/down file pair in the
migrations directory, with corresponding insert
sync - create a script to reload migrations.log from the DB
(run after upgrading sql-migrate)
status - shows the same output as if processing a forward-migration
up [n] - create a script to run pending migrations (ALL by default)
down [n] - create a script to roll back migrations (ONE by default)
@ -135,6 +137,10 @@ NOTE: POSTGRES SCHEMAS
Each schema gets its own _migrations table, so tenants are migrated
independently. PGOPTIONS is supported by psql and all libpq clients.
UPGRADING
After upgrading sql-migrate, run sync to refresh the log format:
sql-migrate -d ./sql/migrations/ sync | sh
`
var (
@ -142,6 +148,22 @@ var (
commentStartRe = regexp.MustCompile(`(^|\s+)#.*`)
)
// logMigrationsSelect returns the DB-specific SELECT line for _migrations.
// The output format is "id<tab>name" per row, compatible with shmigrate.Applied().
func logMigrationsSelect(sqlCommand string) string {
var selectExpr string
switch {
case strings.Contains(sqlCommand, "psql"):
selectExpr = "id || CHR(9) || name"
case strings.Contains(sqlCommand, "mysql") || strings.Contains(sqlCommand, "mariadb"):
selectExpr = "CONCAT(id, CHAR(9), name)"
default:
fmt.Fprintf(os.Stderr, "Error: unrecognized --sql-command %q; cannot generate _migrations.sql\n", sqlCommand)
os.Exit(1)
}
return fmt.Sprintf("SELECT %s FROM _migrations ORDER BY name;", selectExpr)
}
type State struct {
Date time.Time
SQLCommand string
@ -275,6 +297,9 @@ func main() {
os.Exit(1)
}
// auto-upgrade _migrations.sql to include id in output
maybeUpgradeLogQuery(logQueryPath, state.SQLCommand)
logText, err := os.ReadFile(state.LogPath)
if err != nil {
if !os.IsNotExist(err) {
@ -562,7 +587,7 @@ func mustInit(cfg *MainConfig) {
}
logQueryPath := filepath.Join(state.MigrationsDir, LOG_QUERY_NAME)
if created, err := initFile(logQueryPath, LOG_MIGRATIONS_QUERY); err != nil {
if created, err := initFile(logQueryPath, logMigrationsQueryNote+logMigrationsSelect(state.SQLCommand)+"\n"); err != nil {
fmt.Fprintf(os.Stderr, "Error: init couldn't create migrations query: %v\n", err)
os.Exit(1)
} else if created {
@ -578,6 +603,34 @@ func mustInit(cfg *MainConfig) {
}
}
// maybeUpgradeLogQuery replaces the old name-only SELECT in _migrations.sql
// with the new id+name SELECT. Only the matching line is replaced; comments
// and other customizations are preserved.
func maybeUpgradeLogQuery(path, sqlCommand string) {
b, err := os.ReadFile(path)
if err != nil {
return
}
var replaced bool
var lines []string
for line := range strings.SplitSeq(string(b), "\n") {
if !replaced && strings.TrimSpace(line) == logMigrationsQueryPrev2_2_0 {
line = logMigrationsSelect(sqlCommand)
replaced = true
}
lines = append(lines, line)
}
if !replaced {
return
}
if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Warn: couldn't upgrade %s: %v\n", path, err)
return
}
fmt.Fprintf(os.Stderr, " upgraded %s (added id to output)\n", filepathUnclean(path))
}
func initFile(path, contents string) (bool, error) {
if fileExists(path) {
return false, nil