diff --git a/migrate/export_test.go b/migrate/export_test.go index 49db1be..4fad8bd 100644 --- a/migrate/export_test.go +++ b/migrate/export_test.go @@ -3,3 +3,7 @@ package migrate func IsCallback(stmt string) (name string) { return isCallback(stmt) } + +func IsComment(stmt string) bool { + return isComment(stmt) +} diff --git a/migrate/migrate.go b/migrate/migrate.go index 463bd9c..d7ab21f 100644 --- a/migrate/migrate.go +++ b/migrate/migrate.go @@ -201,6 +201,23 @@ func FromFS(ctx context.Context, session gocqlx.Session, f fs.FS) error { return nil } +// applyMigration executes a single migration file by parsing and applying its statements. +// It handles three types of content in migration files: +// - SQL statements: executed against the database +// - Callback commands: processed via registered callback handlers (format: -- CALL function_name;) +// - Regular comments: silently skipped (format: -- any comment text) +// +// The function maintains migration state by tracking the number of completed statements, +// allowing for resumption of partially completed migrations. +// +// Parameters: +// - ctx: context for cancellation and timeouts +// - session: database session for executing statements +// - f: filesystem containing the migration file +// - path: path to the migration file within the filesystem +// - done: number of statements already completed (for resuming partial migrations) +// +// Returns an error if the migration fails at any point. func applyMigration(ctx context.Context, session gocqlx.Session, f fs.FS, path string, done int) error { file, err := f.Open(path) if err != nil { @@ -272,19 +289,23 @@ func applyMigration(ctx context.Context, session gocqlx.Session, f fs.FS, path s // trim new lines and all whitespace characters stmt = strings.TrimSpace(stmt) + // Process statement based on its type if cb := isCallback(stmt); cb != "" { + // Handle callback commands (e.g., "-- CALL function_name;") if Callback == nil { return fmt.Errorf("statement %d: missing callback handler while trying to call %s", i, cb) } if err := Callback(ctx, session, CallComment, cb); err != nil { return fmt.Errorf("callback %s: %s", cb, err) } - } else { + } else if stmt != "" && !isComment(stmt) { + // Execute SQL statements (skip empty statements and comments) q := session.ContextQuery(ctx, stmt, nil).RetryPolicy(nil) if err := q.ExecRelease(); err != nil { return fmt.Errorf("statement %d: %s", i, err) } } + // Regular comments and empty statements are silently skipped // update info info.Done = i @@ -315,3 +336,10 @@ func isCallback(stmt string) (name string) { } return s[1] } + +// isComment returns true if the statement is a SQL comment that should be ignored. +// It distinguishes between regular comments (which should be skipped) and +// callback commands (which should be processed). +func isComment(stmt string) bool { + return strings.HasPrefix(stmt, "--") && isCallback(stmt) == "" +} diff --git a/migrate/migrate_test.go b/migrate/migrate_test.go index f8c5087..e0ed563 100644 --- a/migrate/migrate_test.go +++ b/migrate/migrate_test.go @@ -160,6 +160,29 @@ func TestMigrationNoSemicolon(t *testing.T) { } } +func TestMigrationWithTrailingComment(t *testing.T) { + session := gocqlxtest.CreateSession(t) + defer session.Close() + recreateTables(t, session) + + if err := session.ExecStmt(migrateSchema); err != nil { + t.Fatal(err) + } + + f := makeTestFS(0) + // Create a migration with a trailing comment (this should reproduce the issue) + migrationContent := fmt.Sprintf(insertMigrate, 0) + "; -- ttl 1 hour" + f.WriteFile("0.cql", []byte(migrationContent), fs.ModePerm) + + ctx := context.Background() + if err := migrate.FromFS(ctx, session, f); err != nil { + t.Fatal("Migration should succeed with trailing comment, but got error:", err) + } + if c := countMigrations(t, session); c != 1 { + t.Fatal("expected 1 migration got", c) + } +} + func TestIsCallback(t *testing.T) { table := []struct { Name string @@ -211,6 +234,60 @@ func TestIsCallback(t *testing.T) { } } +func TestIsComment(t *testing.T) { + table := []struct { + Name string + Stmt string + IsComment bool + }{ + { + Name: "CQL statement", + Stmt: "SELECT * from X;", + IsComment: false, + }, + { + Name: "Regular comment", + Stmt: "-- This is a comment", + IsComment: true, + }, + { + Name: "Comment with additional text", + Stmt: "-- ttl 1 hour", + IsComment: true, + }, + { + Name: "Callback command (not a regular comment)", + Stmt: "-- CALL Foo;", + IsComment: false, + }, + { + Name: "Callback with spaces (not a regular comment)", + Stmt: "-- CALL Bar;", + IsComment: false, + }, + { + Name: "Empty statement", + Stmt: "", + IsComment: false, + }, + { + Name: "Whitespace only", + Stmt: " ", + IsComment: false, + }, + } + + for i := range table { + test := table[i] + t.Run(test.Name, func(t *testing.T) { + result := migrate.IsComment(test.Stmt) + if result != test.IsComment { + t.Errorf("IsComment(%q) = %v, expected %v", test.Stmt, result, test.IsComment) + } + }) + } +} + func TestMigrationCallback(t *testing.T) { var ( beforeCalled int