From 3cc1888629ae67a3550d3cf249189f795880b706 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Tue, 6 May 2025 11:13:09 +0800 Subject: [PATCH 1/6] Add enroll_db cli option --- fastmigrate/cli.py | 17 +++++++++++++ fastmigrate/core.py | 61 ++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/fastmigrate/cli.py b/fastmigrate/cli.py index 0d17ab4..2dda417 100644 --- a/fastmigrate/cli.py +++ b/fastmigrate/cli.py @@ -128,6 +128,23 @@ def create_db( print(f"Unexpected error: {e}") sys.exit(1) +@call_parse +def enroll_db( + db: Path = DEFAULT_DB, # Path to the SQLite database file + config_path: Path = DEFAULT_CONFIG # Path to config file +) -> None: + """Convert an unversioned SQLite database to a versioned one. + + Note: command line arguments take precedence over values from a + config file, unless they are equal to default values. + """ + db_path, migrations_path = _get_config(config_path, db) + success = core.enroll_db(db_path) + if not success: + sys.exit(1) + + + @call_parse def run_migrations( db: Path = DEFAULT_DB, # Path to the SQLite database file diff --git a/fastmigrate/core.py b/fastmigrate/core.py index 8f90a12..02395cf 100644 --- a/fastmigrate/core.py +++ b/fastmigrate/core.py @@ -49,7 +49,7 @@ def ensure_versioned_db(db_path:Path) -> int: stacklevel=2) return create_db(db_path) -def _ensure_meta_table(db_path: Path) -> None: +def _ensure_meta_table(db_path: Path): """Create the _meta table if it doesn't exist, with a single row constraint. Uses a single-row pattern with a PRIMARY KEY on a constant value (1). @@ -62,6 +62,9 @@ def _ensure_meta_table(db_path: Path) -> None: Args: db_path: Path to the SQLite database + + Returns: + None Raises: FileNotFoundError: If database file doesn't exist @@ -106,6 +109,62 @@ def _ensure_meta_table(db_path: Path) -> None: if conn: conn.close() +def enroll_db(db_path: Path) -> bool: + """Create the _meta table if it doesn't exist, with a single row constraint. + + Uses a single-row pattern with a PRIMARY KEY on a constant value (1). + This ensures we can only have one row in the table. + + Args: + db_path: Path to the SQLite database + + Returns: + bool: True if the table was created successfully, False otherwise + + Raises: + FileNotFoundError: If database file doesn't exist + sqlite3.Error: If unable to read or write to the database + """ + db_path = Path(db_path) + # First check if the file exists + if not db_path.exists(): + raise FileNotFoundError(f"Database file does not exist: {db_path}") + + conn = None + try: + conn = sqlite3.connect(db_path) + # Check if _meta table exists + cursor = conn.execute( + """ + SELECT name, sql FROM sqlite_master + WHERE type='table' AND name='_meta' + """ + ) + row = cursor.fetchone() + + if row is None: + # Table doesn't exist, create it with version 0 + try: + with conn: + conn.execute( + """ + CREATE TABLE _meta ( + id INTEGER PRIMARY KEY CHECK (id = 1), + version INTEGER NOT NULL DEFAULT 0 + ) + """ + ) + conn.execute("INSERT INTO _meta (id, version) VALUES (1, 0)") + except sqlite3.Error as e: + raise sqlite3.Error(f"Failed to create _meta table: {e}") + return True + else: + return False + except sqlite3.Error as e: + raise sqlite3.Error(f"Failed to access database: {e}") + finally: + if conn: + conn.close() def get_db_version(db_path: Path) -> int: """Get the current database version. diff --git a/pyproject.toml b/pyproject.toml index 1e17d8a..fdee127 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dev = [ fastmigrate_backup_db = "fastmigrate.cli:backup_db" fastmigrate_check_version = "fastmigrate.cli:check_version" fastmigrate_create_db = "fastmigrate.cli:create_db" +fastmigrate_enroll_db = "fastmigrate.cli:enroll_db" fastmigrate_run_migrations = "fastmigrate.cli:run_migrations" [tool.pytest] From ed2942b0953b889ea505558443ba17e4063dfb85 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Tue, 6 May 2025 11:59:19 +0800 Subject: [PATCH 2/6] Tests for enroll_db --- tests/test_cli.py | 361 ++++++++++++++++++++++++++++------------ tests/test_enroll_db.py | 128 ++++++++++++++ 2 files changed, 378 insertions(+), 111 deletions(-) create mode 100644 tests/test_enroll_db.py diff --git a/tests/test_cli.py b/tests/test_cli.py index 0cdd004..2b138c8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -9,7 +9,7 @@ from unittest.mock import patch import subprocess -from fastmigrate.cli import backup_db, check_version, create_db, run_migrations +from fastmigrate.cli import backup_db, check_version, create_db, enroll_db, run_migrations from fastmigrate.core import _ensure_meta_table, _set_db_version # Path to the test migrations directory @@ -17,13 +17,22 @@ def test_cli_help_backup_db(): - """Test the CLI help output for .""" + """Test the CLI help output for backup_db.""" # Capture standard output result = subprocess.run(['fastmigrate_backup_db', '--help'], capture_output=True, text=True) assert result.returncode == 0 assert "usage: fastmigrate_backup_db [-h] [--db DB]" in result.stdout + +def test_cli_help_enroll_db(): + """Test the CLI help output for enroll_db.""" + # Capture standard output + result = subprocess.run(['fastmigrate_enroll_db', '--help'], + capture_output=True, text=True) + assert result.returncode == 0 + assert "usage: fastmigrate_enroll_db [-h] [--db DB]" in result.stdout + def test_cli_explicit_paths(): """Test CLI with explicit path arguments.""" with tempfile.TemporaryDirectory() as temp_dir: @@ -249,122 +258,252 @@ def test_cli_precedence(): assert cursor.fetchone() is None, "CLI DB should not have config_table" conn_cli.close() - +def test_cli_enroll_db_success(tmp_path): + """Test the CLI enroll_db command successfully enrolls an unversioned database.""" + db_path = tmp_path / "unversioned.db" + + # Create an unversioned database with a sample table + conn = sqlite3.connect(db_path) + conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)") + conn.execute("INSERT INTO users (name) VALUES ('test_user')") + conn.commit() + conn.close() + + # Verify _meta table doesn't exist yet + conn = sqlite3.connect(db_path) + cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='_meta'") + assert cursor.fetchone() is None + conn.close() + + # Run the enroll_db command + result = subprocess.run([ + "fastmigrate_enroll_db", + "--db", db_path + ], capture_output=True, text=True) + + assert result.returncode == 0 + + # Verify the database has been enrolled (_meta table created) + conn = sqlite3.connect(db_path) + cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='_meta'") + assert cursor.fetchone() is not None + + # Original data should still be intact + cursor = conn.execute("SELECT name FROM users WHERE name='test_user'") + assert cursor.fetchone() is not None + + # Version should be 0 + cursor = conn.execute("SELECT version FROM _meta WHERE id = 1") + assert cursor.fetchone()[0] == 0 + + conn.close() -def test_cli_createdb_flag(): - """Test the --create_db flag properly initializes a database with _meta table.""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_dir_path = Path(temp_dir) - db_path = temp_dir_path / "new_db.db" - - # Verify the database doesn't exist yet - assert not db_path.exists() - - # Run the CLI with just the --create_db flag - result = subprocess.run([ - "fastmigrate_create_db", - "--db", db_path, - ]) - - assert result.returncode == 0 - - # Verify database was created - assert db_path.exists() - - # Verify the _meta table exists with version 0 - conn = sqlite3.connect(db_path) - cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='_meta'") - assert cursor.fetchone() is not None - - cursor = conn.execute("SELECT version FROM _meta WHERE id = 1") - assert cursor.fetchone()[0] == 0 + +def test_cli_enroll_db_already_versioned(tmp_path): + """Test the CLI enroll_db command fails when the database is already versioned.""" + db_path = tmp_path / "versioned.db" + + # Create a versioned database + conn = sqlite3.connect(db_path) + conn.execute(""" + CREATE TABLE _meta ( + id INTEGER PRIMARY KEY CHECK (id = 1), + version INTEGER NOT NULL DEFAULT 0 + ) + """) + conn.execute("INSERT INTO _meta (id, version) VALUES (1, 42)") + conn.commit() + conn.close() + + # Run the enroll_db command on an already versioned database + result = subprocess.run([ + "fastmigrate_enroll_db", + "--db", db_path + ], capture_output=True, text=True) + + # Should exit with non-zero status because the database is already versioned + assert result.returncode == 1 + + # Verify the database version wasn't changed + conn = sqlite3.connect(db_path) + cursor = conn.execute("SELECT version FROM _meta WHERE id = 1") + assert cursor.fetchone()[0] == 42 + conn.close() + + +def test_cli_enroll_db_nonexistent_db(tmp_path): + """Test the CLI enroll_db command fails when the database doesn't exist.""" + db_path = tmp_path / "nonexistent.db" + + # Verify file doesn't exist + assert not db_path.exists() + + # Run the enroll_db command on a non-existent database + result = subprocess.run([ + "fastmigrate_enroll_db", + "--db", db_path + ], capture_output=True, text=True) + + # Should exit with non-zero status + assert result.returncode == 1 + assert "does not exist" in result.stdout or "does not exist" in result.stderr + + +def test_cli_enroll_db_invalid_db(tmp_path): + """Test the CLI enroll_db command fails when the database is invalid.""" + db_path = tmp_path / "invalid.db" + + # Create an invalid database file + with open(db_path, 'wb') as f: + f.write(b'This is not a valid SQLite database') + + # Run the enroll_db command on an invalid database + result = subprocess.run([ + "fastmigrate_enroll_db", + "--db", db_path + ], capture_output=True, text=True) + + # Should exit with non-zero status + assert result.returncode == 1 + + +def test_cli_enroll_db_with_config_file(tmp_path): + """Test the CLI enroll_db command with configuration from a file.""" + db_path = tmp_path / "db_from_config.db" + config_path = tmp_path / "test_config.ini" + + # Create an unversioned database + conn = sqlite3.connect(db_path) + conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)") + conn.commit() + conn.close() + + # Create a config file + config_path.write_text(f"[paths]\ndb = {db_path}") + + # Run the enroll_db command with config + result = subprocess.run([ + "fastmigrate_enroll_db", + "--config", config_path + ], capture_output=True, text=True) + + assert result.returncode == 0 + + # Verify the database has been enrolled + conn = sqlite3.connect(db_path) + cursor = conn.execute("SELECT version FROM _meta WHERE id = 1") + assert cursor.fetchone()[0] == 0 + conn.close() - conn.close() + +def test_cli_createdb_flag(tmp_path): + """Test the --create_db flag properly initializes a database with _meta table.""" + db_path = tmp_path / "new_db.db" + + # Verify the database doesn't exist yet + assert not db_path.exists() + + # Run the CLI with just the --create_db flag + result = subprocess.run([ + "fastmigrate_create_db", + "--db", db_path, + ]) + + assert result.returncode == 0 + + # Verify database was created + assert db_path.exists() + + # Verify the _meta table exists with version 0 + conn = sqlite3.connect(db_path) + cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='_meta'") + assert cursor.fetchone() is not None + + cursor = conn.execute("SELECT version FROM _meta WHERE id = 1") + assert cursor.fetchone()[0] == 0 + + conn.close() -def test_check_db_version_option(): +def test_check_db_version_option(tmp_path): """Test the --check_db_version option correctly reports the database version.""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_dir_path = Path(temp_dir) - db_path = temp_dir_path / "test.db" - - # Create database file with version 42 - conn = sqlite3.connect(db_path) - conn.close() - _ensure_meta_table(db_path) - _set_db_version(db_path, 42) - - # Test with versioned database - result = subprocess.run([ - "fastmigrate_check_version", - "--db", db_path - ], capture_output=True, text=True) - - assert result.returncode == 0 - assert "Database version: 42" in result.stdout - - # Create unversioned database - unversioned_db = temp_dir_path / "unversioned.db" - conn = sqlite3.connect(unversioned_db) - conn.close() - - # Test with unversioned database - result = subprocess.run([ - "fastmigrate_check_version", - "--db", unversioned_db, - ], capture_output=True, text=True) - - assert result.returncode == 0 - assert "unversioned" in result.stdout.lower() - - # Test with non-existent database - nonexistent_db = temp_dir_path / "nonexistent.db" - result = subprocess.run([ - "fastmigrate_check_version", - "--db", nonexistent_db, - ], capture_output=True, text=True) - - assert result.returncode == 1 - assert "does not exist" in result.stdout + db_path = tmp_path / "test.db" + + # Create database file with version 42 + conn = sqlite3.connect(db_path) + conn.close() + _ensure_meta_table(db_path) + _set_db_version(db_path, 42) + + # Test with versioned database + result = subprocess.run([ + "fastmigrate_check_version", + "--db", db_path + ], capture_output=True, text=True) + + assert result.returncode == 0 + assert "Database version: 42" in result.stdout + + # Create unversioned database + unversioned_db = tmp_path / "unversioned.db" + conn = sqlite3.connect(unversioned_db) + conn.close() + + # Test with unversioned database + result = subprocess.run([ + "fastmigrate_check_version", + "--db", unversioned_db, + ], capture_output=True, text=True) + + assert result.returncode == 0 + assert "unversioned" in result.stdout.lower() + + # Test with non-existent database + nonexistent_db = tmp_path / "nonexistent.db" + result = subprocess.run([ + "fastmigrate_check_version", + "--db", nonexistent_db, + ], capture_output=True, text=True) + + assert result.returncode == 1 + assert "does not exist" in result.stdout -def test_cli_with_testsuite_a(): +def test_cli_with_testsuite_a(tmp_path): """Test CLI using testsuite_a.""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_dir_path = Path(temp_dir) - db_path = temp_dir_path / "test.db" - - # Create empty database file - conn = sqlite3.connect(db_path) - conn.close() - - # Initialize the database with _meta table - _ensure_meta_table(db_path) - - # Run the CLI with explicit paths to the test suite - result = subprocess.run([ - "fastmigrate_run_migrations", - "--db", db_path, - "--migrations", CLI_MIGRATIONS_DIR / "migrations" - ], capture_output=True, text=True) - - assert result.returncode == 0 - - # Verify migrations applied - conn = sqlite3.connect(db_path) - - # Version should be 4 (all migrations applied) - cursor = conn.execute("SELECT version FROM _meta") - assert cursor.fetchone()[0] == 4 - - # Verify tables exist - tables = ["users", "posts", "tags", "post_tags"] - for table in tables: - cursor = conn.execute( - f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table}'" - ) - assert cursor.fetchone() is not None - - conn.close() + db_path = tmp_path / "test.db" + + # Create empty database file + conn = sqlite3.connect(db_path) + conn.close() + + # Initialize the database with _meta table + _ensure_meta_table(db_path) + + # Run the CLI with explicit paths to the test suite + result = subprocess.run([ + "fastmigrate_run_migrations", + "--db", db_path, + "--migrations", CLI_MIGRATIONS_DIR / "migrations" + ], capture_output=True, text=True) + + assert result.returncode == 0 + + # Verify migrations applied + conn = sqlite3.connect(db_path) + + # Version should be 4 (all migrations applied) + cursor = conn.execute("SELECT version FROM _meta") + assert cursor.fetchone()[0] == 4 + + # Verify tables exist + tables = ["users", "posts", "tags", "post_tags"] + for table in tables: + cursor = conn.execute( + f"SELECT name FROM sqlite_master WHERE type='table' AND name='{table}'" + ) + assert cursor.fetchone() is not None + + conn.close() diff --git a/tests/test_enroll_db.py b/tests/test_enroll_db.py new file mode 100644 index 0000000..6b5a911 --- /dev/null +++ b/tests/test_enroll_db.py @@ -0,0 +1,128 @@ +"""Tests for the enroll_db functionality of fastmigrate.""" + +import os +import sqlite3 +import tempfile +from pathlib import Path + +import pytest + +from fastmigrate.core import enroll_db, get_db_version + + +def test_enroll_db_on_new_db(tmp_path): + """Test enrolling a newly created database.""" + # Create a temp file database for testing + db_path = tmp_path / "test_enroll.db" + + # Create empty database file + conn = sqlite3.connect(db_path) + conn.close() + + # Enroll the database + result = enroll_db(db_path) + + # Connect and check results + conn = sqlite3.connect(db_path) + + # Check the table exists + cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='_meta'") + assert cursor.fetchone() is not None + + # Check there's one row + cursor = conn.execute("SELECT COUNT(*) FROM _meta") + assert cursor.fetchone()[0] == 1 + + # Check the version is 0 + cursor = conn.execute("SELECT version FROM _meta WHERE id = 1") + assert cursor.fetchone()[0] == 0 + + # Check result is True (table was created) + assert result is True + + # Also verify using get_db_version + assert get_db_version(db_path) == 0 + + conn.close() + + +def test_enroll_db_on_versioned_db(tmp_path): + """Test enrolling a database that is already versioned.""" + # Create a temp file database for testing + db_path = tmp_path / "test_enroll.db" + + # Create a versioned database + conn = sqlite3.connect(db_path) + conn.execute(""" + CREATE TABLE _meta ( + id INTEGER PRIMARY KEY CHECK (id = 1), + version INTEGER NOT NULL DEFAULT 0 + ) + """) + conn.execute("INSERT INTO _meta (id, version) VALUES (1, 42)") + conn.commit() + conn.close() + + # Try to enroll the already versioned database + result = enroll_db(db_path) + + # Check result is False (table already existed) + assert result is False + + # Verify version wasn't changed + assert get_db_version(db_path) == 42 + + +def test_enroll_db_on_unversioned_db_with_tables(tmp_path): + """Test enrolling an existing database with tables but no version tracking.""" + # Create a temp file database for testing + db_path = tmp_path / "test_enroll.db" + + # Create an unversioned database with a sample table + conn = sqlite3.connect(db_path) + conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)") + conn.execute("INSERT INTO users (name) VALUES ('test_user')") + conn.commit() + conn.close() + + # Enroll the database + result = enroll_db(db_path) + + # Check result is True (table was created) + assert result is True + + # Verify the version is 0 + assert get_db_version(db_path) == 0 + + # Verify the sample table still exists + conn = sqlite3.connect(db_path) + cursor = conn.execute("SELECT name FROM users") + assert cursor.fetchone()[0] == 'test_user' + conn.close() + + +def test_enroll_db_nonexistent_file(tmp_path): + """Test enrolling a database that doesn't exist.""" + # Create a path to a non-existent database + db_path = tmp_path / "nonexistent.db" + + # Verify file doesn't exist + assert not db_path.exists() + + # Try to enroll the non-existent database (should raise FileNotFoundError) + with pytest.raises(FileNotFoundError): + enroll_db(db_path) + + +def test_enroll_db_invalid_db(tmp_path): + """Test enrolling an invalid database file.""" + # Create a temp file with invalid content + db_path = tmp_path / "bad.db" + + # Write some invalid binary data + with open(db_path, 'wb') as f: + f.write(b'This is not a valid SQLite database') + + # Try to enroll the invalid database (should raise sqlite3.Error) + with pytest.raises(sqlite3.Error): + enroll_db(db_path) \ No newline at end of file From 6929fec438beb59cd653e773492fb4541e0b1833 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Tue, 6 May 2025 13:57:33 +0800 Subject: [PATCH 3/6] Replace use of `_ensure_meta_table` with `enroll_db` --- README.md | 6 ++ enrolling.md | 4 +- fastmigrate/cli.py | 2 +- fastmigrate/core.py | 68 +++---------------- tests/test_cli.py | 14 ++-- .../test_comprehensive/test_comprehensive.py | 4 +- tests/test_core.py | 12 ++-- tests/test_failures.py | 12 ++-- tests/test_migrations.py | 10 +-- tests/test_selective_migrations.py | 10 +-- tests/test_transactions.py | 6 +- 11 files changed, 53 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 4192a5a..584cf76 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,12 @@ Here are some commands: ``` Run all needed migrations on the db. Fails if a migration fails, or if there is no managed db at the path. This is equivalent to calling `fastmigrate.run_migrations()` +5. **Enroll an existing db**: + ``` + fastmigrate_enroll_db --db path/to/data.db + ``` + This will create a new versioned database at the path, and copy the contents of the existing database into it. This is equivalent to calling `fastmigrate.enroll_db()` + ### How to enroll an existing, unversioned database into fastmigrate diff --git a/enrolling.md b/enrolling.md index 998e220..f991c3d 100644 --- a/enrolling.md +++ b/enrolling.md @@ -23,8 +23,8 @@ But if you are enrolling an existing db into fastmigrate, then you need to do th - Second, manually modify your current data to add fastmigrate version tag and set its version to 1. You can do this by using fastmigrate's internal API. Doing this constitutes asserting that the db is in fact in the state which would be produced by the migration script 0001. After doing this, fastmigrate will recognize your db as managed. Here is how to do it: ```python -from fastmigrate.core import _ensure_meta_table, _set_db_version -_ensure_meta_table("path/to/data.db") +from fastmigrate.core import enroll_db, _set_db_version +enroll_db("path/to/data.db") _set_db_version("path/to/data.db",1) ``` diff --git a/fastmigrate/cli.py b/fastmigrate/cli.py index 2dda417..0229321 100644 --- a/fastmigrate/cli.py +++ b/fastmigrate/cli.py @@ -128,6 +128,7 @@ def create_db( print(f"Unexpected error: {e}") sys.exit(1) + @call_parse def enroll_db( db: Path = DEFAULT_DB, # Path to the SQLite database file @@ -144,7 +145,6 @@ def enroll_db( sys.exit(1) - @call_parse def run_migrations( db: Path = DEFAULT_DB, # Path to the SQLite database file diff --git a/fastmigrate/core.py b/fastmigrate/core.py index 02395cf..a7b5ad5 100644 --- a/fastmigrate/core.py +++ b/fastmigrate/core.py @@ -36,7 +36,7 @@ def create_db(db_path:Path) -> int: db_path.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(db_path) conn.close() - _ensure_meta_table(db_path) + enroll_db(db_path) return 0 else: return get_db_version(db_path) @@ -49,7 +49,14 @@ def ensure_versioned_db(db_path:Path) -> int: stacklevel=2) return create_db(db_path) -def _ensure_meta_table(db_path: Path): +def _ensure_meta_table(db_path:Path) -> None: + "See enroll_db" + warnings.warn("_ensure_meta_table is deprecated, as it has been renamed to enroll_db, which is identical except adding a boolean return value", + DeprecationWarning, + stacklevel=2) + return enroll_db(db_path) + +def enroll_db(db_path: Path) -> bool: """Create the _meta table if it doesn't exist, with a single row constraint. Uses a single-row pattern with a PRIMARY KEY on a constant value (1). @@ -58,62 +65,7 @@ def _ensure_meta_table(db_path: Path): WARNING: users should call this directly only if preparing a versioned db manually, for instance, for testing or for enrolling a non-version db after verifying its values and schema are what - would be produced by migration scripts. - - Args: - db_path: Path to the SQLite database - - Returns: - None - - Raises: - FileNotFoundError: If database file doesn't exist - sqlite3.Error: If unable to read or write to the database - - """ - db_path = Path(db_path) - # First check if the file exists - if not db_path.exists(): - raise FileNotFoundError(f"Database file does not exist: {db_path}") - - conn = None - try: - conn = sqlite3.connect(db_path) - # Check if _meta table exists - cursor = conn.execute( - """ - SELECT name, sql FROM sqlite_master - WHERE type='table' AND name='_meta' - """ - ) - row = cursor.fetchone() - - if row is None: - # Table doesn't exist, create it with version 0 - try: - with conn: - conn.execute( - """ - CREATE TABLE _meta ( - id INTEGER PRIMARY KEY CHECK (id = 1), - version INTEGER NOT NULL DEFAULT 0 - ) - """ - ) - conn.execute("INSERT INTO _meta (id, version) VALUES (1, 0)") - except sqlite3.Error as e: - raise sqlite3.Error(f"Failed to create _meta table: {e}") - except sqlite3.Error as e: - raise sqlite3.Error(f"Failed to access database: {e}") - finally: - if conn: - conn.close() - -def enroll_db(db_path: Path) -> bool: - """Create the _meta table if it doesn't exist, with a single row constraint. - - Uses a single-row pattern with a PRIMARY KEY on a constant value (1). - This ensures we can only have one row in the table. + would be produced by migration scripts. Args: db_path: Path to the SQLite database diff --git a/tests/test_cli.py b/tests/test_cli.py index 2b138c8..547a489 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -10,7 +10,7 @@ import subprocess from fastmigrate.cli import backup_db, check_version, create_db, enroll_db, run_migrations -from fastmigrate.core import _ensure_meta_table, _set_db_version +from fastmigrate.core import enroll_db, _set_db_version # Path to the test migrations directory CLI_MIGRATIONS_DIR = Path(__file__).parent / "test_cli" @@ -50,7 +50,7 @@ def test_cli_explicit_paths(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(db_path) + enroll_db(db_path) # Create a migration with open(migrations_dir / "0001-test.sql", "w") as f: @@ -93,7 +93,7 @@ def test_cli_backup_option(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(db_path) + enroll_db(db_path) # Create a test migration with open(migrations_path / "0001-test.sql", "w") as f: @@ -163,7 +163,7 @@ def test_cli_config_file(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(db_path) + enroll_db(db_path) # Create a migration (migrations_dir / "0001-test.sql").write_text("CREATE TABLE custom_config (id INTEGER PRIMARY KEY);") @@ -215,7 +215,7 @@ def test_cli_precedence(): conn = sqlite3.connect(db) conn.close() # Initialize the database with _meta table - _ensure_meta_table(db) + enroll_db(db) # Create different migrations in each directory with open(migrations_config / "0001-config.sql", "w") as f: @@ -434,7 +434,7 @@ def test_check_db_version_option(tmp_path): # Create database file with version 42 conn = sqlite3.connect(db_path) conn.close() - _ensure_meta_table(db_path) + enroll_db(db_path) _set_db_version(db_path, 42) # Test with versioned database @@ -480,7 +480,7 @@ def test_cli_with_testsuite_a(tmp_path): conn.close() # Initialize the database with _meta table - _ensure_meta_table(db_path) + enroll_db(db_path) # Run the CLI with explicit paths to the test suite result = subprocess.run([ diff --git a/tests/test_comprehensive/test_comprehensive.py b/tests/test_comprehensive/test_comprehensive.py index 03742b6..e27b5cf 100644 --- a/tests/test_comprehensive/test_comprehensive.py +++ b/tests/test_comprehensive/test_comprehensive.py @@ -26,7 +26,7 @@ from pathlib import Path from fastmigrate.core import ( - _ensure_meta_table, get_db_version, _set_db_version, + enroll_db, get_db_version, _set_db_version, get_migration_scripts, run_migrations ) @@ -51,7 +51,7 @@ def setUp(self): self.conn.close() # Initialize the _meta table (should be version 0) - _ensure_meta_table(self.db_path) + enroll_db(self.db_path) # Verify we're starting with version 0 self.assertEqual(get_db_version(self.db_path), 0, diff --git a/tests/test_core.py b/tests/test_core.py index 0589b44..6067ecb 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -9,7 +9,7 @@ import pytest from fastmigrate.core import ( - _ensure_meta_table, + enroll_db, create_db, get_db_version, _set_db_version, @@ -20,14 +20,14 @@ ) -def test_ensure_meta_table(): +def testenroll_db(): """Test ensuring the _meta table exists.""" # Create a temp file database for testing with tempfile.NamedTemporaryFile(suffix='.db') as temp_file: db_path = Path(temp_file.name) - # Call _ensure_meta_table on the path - _ensure_meta_table(db_path) + # Call enroll_db on the path + enroll_db(db_path) # Connect and check results conn = sqlite3.connect(db_path) @@ -62,7 +62,7 @@ def test_ensure_meta_table(): # Test with invalid path to verify exception is raised with pytest.raises(FileNotFoundError): - _ensure_meta_table(Path("/nonexistent/path/to/db.db")) + enroll_db(Path("/nonexistent/path/to/db.db")) def test_get_set_db_version(): # Tests the internal _set_db_version function @@ -72,7 +72,7 @@ def test_get_set_db_version(): # Tests the internal _set_db_version function db_path = Path(temp_file.name) # Initialize the database first - _ensure_meta_table(db_path) + enroll_db(db_path) # Initial version should be 0 assert get_db_version(db_path) == 0 diff --git a/tests/test_failures.py b/tests/test_failures.py index 76b9a01..a139bff 100644 --- a/tests/test_failures.py +++ b/tests/test_failures.py @@ -7,7 +7,7 @@ import pytest -from fastmigrate.core import run_migrations, _ensure_meta_table +from fastmigrate.core import run_migrations, enroll_db # Path to the migrations directory @@ -25,7 +25,7 @@ def test_sql_failure(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(db_path) + enroll_db(db_path) # Run migrations - should fail on the second migration result = run_migrations(db_path, migrations_dir) @@ -63,7 +63,7 @@ def test_cli_sql_failure(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(db_path) + enroll_db(db_path) # Run the CLI with path to the failure test suite result = subprocess.run([ @@ -95,7 +95,7 @@ def test_python_failure(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(db_path) + enroll_db(db_path) # Create a test database with initial successful migration initial_migration = migrations_dir / "0001-init.sql" @@ -139,7 +139,7 @@ def test_shell_failure(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(db_path) + enroll_db(db_path) # Create a test database with initial successful migration initial_migration = migrations_dir / "0001-init.sql" @@ -188,7 +188,7 @@ def test_testsuite_failure_cli(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(db_path) + enroll_db(db_path) # Create a temporary migrations directory with just the successful migration # and the specific failure migration we want to test diff --git a/tests/test_migrations.py b/tests/test_migrations.py index bf7ee36..9031e8b 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -7,7 +7,7 @@ import pytest -from fastmigrate.core import run_migrations, _ensure_meta_table +from fastmigrate.core import run_migrations, enroll_db def test_run_migrations_sql(): @@ -24,7 +24,7 @@ def test_run_migrations_sql(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(db_path) + enroll_db(db_path) # Create SQL migration files with open(os.path.join(migrations_dir, "0001-create-table.sql"), "w") as f: @@ -93,7 +93,7 @@ def test_run_migrations_python(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(db_path) + enroll_db(db_path) # Create a base SQL migration with open(os.path.join(migrations_dir, "0001-create-table.sql"), "w") as f: @@ -152,7 +152,7 @@ def test_run_migrations_failed(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(db_path) + enroll_db(db_path) # Create a valid migration with open(os.path.join(migrations_dir, "0001-create-table.sql"), "w") as f: @@ -193,7 +193,7 @@ def test_testsuite_a(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(str(db_path)) + enroll_db(str(db_path)) # Run migrations assert run_migrations(str(db_path), str(migrations_dir)) is True diff --git a/tests/test_selective_migrations.py b/tests/test_selective_migrations.py index d68c25a..3465b40 100644 --- a/tests/test_selective_migrations.py +++ b/tests/test_selective_migrations.py @@ -8,7 +8,7 @@ import pytest -from fastmigrate.core import run_migrations, _ensure_meta_table +from fastmigrate.core import run_migrations, enroll_db # Path to the selective migrations directory @@ -29,7 +29,7 @@ def test_selective_migrations_core(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(str(db_path)) + enroll_db(str(db_path)) # First run: should apply all migrations (0001 through 0010) assert run_migrations(str(db_path), str(migrations_dir)) is True @@ -99,7 +99,7 @@ def test_selective_migrations_resume_after_failure(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(str(db_path)) + enroll_db(str(db_path)) # Create initial migration with open(migrations_dir / "0001-initial.sql", "w") as f: @@ -181,7 +181,7 @@ def test_selective_migrations_with_gaps(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(str(db_path)) + enroll_db(str(db_path)) # Run migrations assert run_migrations(str(db_path), str(migrations_dir)) is True @@ -210,7 +210,7 @@ def test_cli_selective_migrations(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(str(db_path)) + enroll_db(str(db_path)) # Create a temporary migrations directory with just one initial migration migrations_dir = Path(temp_dir) / "migrations" diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 9d75f33..f737f7b 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -7,7 +7,7 @@ import pytest -from fastmigrate.core import run_migrations, _ensure_meta_table +from fastmigrate.core import run_migrations, enroll_db def test_migration_success(): @@ -22,7 +22,7 @@ def test_migration_success(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(str(db_path)) + enroll_db(str(db_path)) # Create first migration with open(migrations_dir / "0001-initial.sql", "w") as f: @@ -86,7 +86,7 @@ def test_migration_failure(): conn.close() # Initialize the database with _meta table - _ensure_meta_table(str(db_path)) + enroll_db(str(db_path)) # Create first migration with open(migrations_dir / "0001-initial.sql", "w") as f: From 4c0ff27cdc27410a9544543ff209b82c9cd7e492 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Tue, 6 May 2025 15:00:58 +0800 Subject: [PATCH 4/6] Add safety checks to enroll_db --- README.md | 6 ++++- fastmigrate/cli.py | 62 ++++++++++++++++++++++++++++++++++++++++++--- fastmigrate/core.py | 57 ++++++++++++++++++++++++++++++++++++++--- pyproject.toml | 1 + 4 files changed, 117 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 584cf76..26026a9 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,11 @@ Here are some commands: ``` fastmigrate_enroll_db --db path/to/data.db ``` - This will create a new versioned database at the path, and copy the contents of the existing database into it. This is equivalent to calling `fastmigrate.enroll_db()` + This will create a new versioned database at the path, and copy the contents of the existing database into it. This is equivalent to calling `fastmigrate.enroll_db()`. + + This command will offer to create an initial migration script for you, based on the current schema of the database. + + ### How to enroll an existing, unversioned database into fastmigrate diff --git a/fastmigrate/cli.py b/fastmigrate/cli.py index 0229321..e145784 100644 --- a/fastmigrate/cli.py +++ b/fastmigrate/cli.py @@ -9,6 +9,10 @@ from importlib.metadata import version from fastmigrate import core +from rich.console import Console + +# Initialize Rich console +console = Console() # Define constants - single source of truth for default values DEFAULT_DB = Path("data/database.db") @@ -131,18 +135,52 @@ def create_db( @call_parse def enroll_db( - db: Path = DEFAULT_DB, # Path to the SQLite database file - config_path: Path = DEFAULT_CONFIG # Path to config file + db:Path=DEFAULT_DB, # Path to the SQLite database file + migrations:Path=DEFAULT_MIGRATIONS, # migrations dir, which may not exist + config_path:Path=DEFAULT_CONFIG, # Path to config file + force:bool=False # Force enrollment even if no migrations are available ) -> None: """Convert an unversioned SQLite database to a versioned one. Note: command line arguments take precedence over values from a config file, unless they are equal to default values. """ - db_path, migrations_path = _get_config(config_path, db) - success = core.enroll_db(db_path) + db_path, migrations_path = _get_config(config_path, db, migrations) + if not db_path.exists(): + raise FileNotFoundError(f"Database file does not exist: {db_path}") + + initial_migration_path = migrations_path / "0001_initial.sql" + if not force and not migrations_path.exists(): + console.print(f"[red bold]No migrations directory or initial migration exists.[/red bold]") + while True: + answer = input(f"Want fastmigrate to generate the initial migration? [Y/n] ").strip().lower() + if answer.lower() in ('y', 'yes', ''): + migrations_path.mkdir(parents=True, exist_ok=True) + console.print(f"Created directory: {migrations_path}") + schema = core.get_db_schema(db_path) + initial_migration_path.write_text(schema) + print(f"Created initial migration at '{initial_migration_path}'") + break + elif answer.lower() in ('n', 'no'): + answer2 = input(f"We [bold]strongly[/bold] recommend creating a migrations directory.\nWant to continue anyway? [N/y] ").strip().lower() + if answer2.lower() in ('n', 'no', ''): + sys.exit(1) + elif answer2.lower() in ('y', 'yes'): + console.print("[red]Continuing without a migrations directory or initial migration.[/red]") + break + else: + console.print("Invalid input. Please enter 'y' or 'n'.") + + try: + success = core.enroll_db(db_path) + except core.MetaTableExists: + console.print(f"[red]Database at {db_path} is already versioned.[/red]") + sys.exit(1) if not success: + console.print(f"[red]Failed to enroll the database at {db_path}.[/red]") sys.exit(1) + console.print(f"[bold]Database at {db_path} is now versioned.[/bold]") + @call_parse @@ -161,3 +199,19 @@ def run_migrations( if not success: sys.exit(1) + +@call_parse +def get_db_schema( + db: Path = DEFAULT_DB, # Path to the SQLite database file + config_path: Path = DEFAULT_CONFIG # Path to config file +) -> None: + """Get the schema of the SQLite database. + + Note: command line arguments take precedence over values from a + config file, unless they are equal to default values. + """ + db_path, migrations_path = _get_config(config_path, db) + schema = core.get_db_schema(db_path) + print(schema) + # if not success: + # sys.exit(1) \ No newline at end of file diff --git a/fastmigrate/core.py b/fastmigrate/core.py index a7b5ad5..43296b3 100644 --- a/fastmigrate/core.py +++ b/fastmigrate/core.py @@ -9,14 +9,15 @@ import warnings from datetime import datetime from pathlib import Path -from typing import Dict, Optional +from typing import Dict, List, Optional, Tuple from rich.console import Console # Initialize Rich console console = Console() -__all__ = ["run_migrations", "create_db", "get_db_version", "create_db_backup", +__all__ = ["run_migrations", "create_db", "get_db_version", + "create_db_backup", "get_db_schema", "enroll_db", "MetaTableExists" # deprecated "ensure_versioned_db", "create_database_backup"] @@ -54,7 +55,9 @@ def _ensure_meta_table(db_path:Path) -> None: warnings.warn("_ensure_meta_table is deprecated, as it has been renamed to enroll_db, which is identical except adding a boolean return value", DeprecationWarning, stacklevel=2) - return enroll_db(db_path) + return enroll_db(db_path) + +class MetaTableExists(Exception): pass def enroll_db(db_path: Path) -> bool: """Create the _meta table if it doesn't exist, with a single row constraint. @@ -111,7 +114,7 @@ def enroll_db(db_path: Path) -> bool: raise sqlite3.Error(f"Failed to create _meta table: {e}") return True else: - return False + raise MetaTableExists("_meta table already exists") except sqlite3.Error as e: raise sqlite3.Error(f"Failed to access database: {e}") finally: @@ -371,6 +374,52 @@ def create_database_backup(db_path:Path) -> Path | None: return create_db_backup(db_path) +def get_db_schema(db_path: Path) -> str: + """Get the SQL schema of a SQLite database file. + + This function retrieves the CREATE statements for all tables, + indices, triggers, and views in the database. + + Args: + db_path: Path to the SQLite database file + + Returns: + str: The SQL schema as a string + + Raises: + FileNotFoundError: If database file doesn't exist + sqlite3.Error: If unable to access the database + """ + db_path = Path(db_path) + # First check if the file exists + if not db_path.exists(): + raise FileNotFoundError(f"Database file does not exist: {db_path}") + + conn = None + try: + conn = sqlite3.connect(db_path) + sqls = [] + # Get schema information for all objects + for row in conn.execute( + "select sql from sqlite_master where sql is not null" + ).fetchall(): + sql = row[0] + if 'sqlite_stat1' in sql: continue + if 'sqlite_stat4' in sql: continue + if 'CREATE TABLE' in sql: sql = sql.replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS") + if 'CREATE INDEX' in sql: sql = sql.replace("CREATE INDEX", "CREATE INDEX IF NOT EXISTS") + if not sql.strip().endswith(";"): + sql += ";" + sqls.append(sql) + return "\n".join(sqls) + + except sqlite3.Error as e: + raise sqlite3.Error(f"Failed to retrieve database schema: {e}") + finally: + if conn: + conn.close() + + def execute_migration_script(db_path: Path, script_path: Path) -> bool: """Execute a migration script based on its file extension.""" db_path = Path(db_path) diff --git a/pyproject.toml b/pyproject.toml index fdee127..b644143 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ fastmigrate_backup_db = "fastmigrate.cli:backup_db" fastmigrate_check_version = "fastmigrate.cli:check_version" fastmigrate_create_db = "fastmigrate.cli:create_db" fastmigrate_enroll_db = "fastmigrate.cli:enroll_db" +fastmigrate_get_db_schema = "fastmigrate.cli:get_db_schema" fastmigrate_run_migrations = "fastmigrate.cli:run_migrations" [tool.pytest] From 8587bab7e29ab301a7bc43de0ce5f6e34407ae96 Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Wed, 7 May 2025 07:19:58 +0800 Subject: [PATCH 5/6] Test cleanup --- fastmigrate/core.py | 15 +++++++++++---- tests/test_cli.py | 8 ++++---- tests/test_enroll_db.py | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/fastmigrate/core.py b/fastmigrate/core.py index 43296b3..c369220 100644 --- a/fastmigrate/core.py +++ b/fastmigrate/core.py @@ -37,7 +37,7 @@ def create_db(db_path:Path) -> int: db_path.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(db_path) conn.close() - enroll_db(db_path) + enroll_db(db_path, err_if_versioned=False) return 0 else: return get_db_version(db_path) @@ -55,16 +55,20 @@ def _ensure_meta_table(db_path:Path) -> None: warnings.warn("_ensure_meta_table is deprecated, as it has been renamed to enroll_db, which is identical except adding a boolean return value", DeprecationWarning, stacklevel=2) - return enroll_db(db_path) + return enroll_db(db_path, err_if_versioned=False) class MetaTableExists(Exception): pass -def enroll_db(db_path: Path) -> bool: +def enroll_db(db_path: Path, err_if_versioned:bool=True) -> bool: """Create the _meta table if it doesn't exist, with a single row constraint. Uses a single-row pattern with a PRIMARY KEY on a constant value (1). This ensures we can only have one row in the table. + Depending on how this function is called, it may raise an error if the + table already exists or return 0. This allows different levels of verbosity + to be used in different contexts. + WARNING: users should call this directly only if preparing a versioned db manually, for instance, for testing or for enrolling a non-version db after verifying its values and schema are what @@ -72,6 +76,8 @@ def enroll_db(db_path: Path) -> bool: Args: db_path: Path to the SQLite database + err_if_versioned: If True, raises an error if the table already exists. + If False, returns False if the table exists. Returns: bool: True if the table was created successfully, False otherwise @@ -114,7 +120,8 @@ def enroll_db(db_path: Path) -> bool: raise sqlite3.Error(f"Failed to create _meta table: {e}") return True else: - raise MetaTableExists("_meta table already exists") + if err_if_versioned: raise MetaTableExists("_meta table already exists") + else: return False except sqlite3.Error as e: raise sqlite3.Error(f"Failed to access database: {e}") finally: diff --git a/tests/test_cli.py b/tests/test_cli.py index 547a489..2d5702d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -280,7 +280,7 @@ def test_cli_enroll_db_success(tmp_path): # Run the enroll_db command result = subprocess.run([ "fastmigrate_enroll_db", - "--db", db_path + "--db", db_path, '--force' ], capture_output=True, text=True) assert result.returncode == 0 @@ -320,7 +320,7 @@ def test_cli_enroll_db_already_versioned(tmp_path): # Run the enroll_db command on an already versioned database result = subprocess.run([ "fastmigrate_enroll_db", - "--db", db_path + "--db", db_path, '--force' ], capture_output=True, text=True) # Should exit with non-zero status because the database is already versioned @@ -343,7 +343,7 @@ def test_cli_enroll_db_nonexistent_db(tmp_path): # Run the enroll_db command on a non-existent database result = subprocess.run([ "fastmigrate_enroll_db", - "--db", db_path + "--db", db_path, '--force' ], capture_output=True, text=True) # Should exit with non-zero status @@ -386,7 +386,7 @@ def test_cli_enroll_db_with_config_file(tmp_path): # Run the enroll_db command with config result = subprocess.run([ "fastmigrate_enroll_db", - "--config", config_path + "--config", config_path, '--force' ], capture_output=True, text=True) assert result.returncode == 0 diff --git a/tests/test_enroll_db.py b/tests/test_enroll_db.py index 6b5a911..9949703 100644 --- a/tests/test_enroll_db.py +++ b/tests/test_enroll_db.py @@ -64,7 +64,7 @@ def test_enroll_db_on_versioned_db(tmp_path): conn.close() # Try to enroll the already versioned database - result = enroll_db(db_path) + result = enroll_db(db_path, err_if_versioned=False) # Check result is False (table already existed) assert result is False From 53f39fa480d3a689bacea4b962099e40df03fc8c Mon Sep 17 00:00:00 2001 From: Daniel Roy Greenfeld Date: Fri, 9 May 2025 16:26:16 +0800 Subject: [PATCH 6/6] Code cleanup --- fastmigrate/cli.py | 3 ++- fastmigrate/core.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastmigrate/cli.py b/fastmigrate/cli.py index e145784..615c818 100644 --- a/fastmigrate/cli.py +++ b/fastmigrate/cli.py @@ -159,7 +159,7 @@ def enroll_db( console.print(f"Created directory: {migrations_path}") schema = core.get_db_schema(db_path) initial_migration_path.write_text(schema) - print(f"Created initial migration at '{initial_migration_path}'") + console.print(f"Created initial migration at '{initial_migration_path}'") break elif answer.lower() in ('n', 'no'): answer2 = input(f"We [bold]strongly[/bold] recommend creating a migrations directory.\nWant to continue anyway? [N/y] ").strip().lower() @@ -197,6 +197,7 @@ def run_migrations( db_path, migrations_path = _get_config(config_path, db, migrations) success = core.run_migrations(db_path, migrations_path, verbose=True) if not success: + console.print(f"Ran migrations from {migrations_path} to {db_path}") sys.exit(1) diff --git a/fastmigrate/core.py b/fastmigrate/core.py index c369220..3d14ab1 100644 --- a/fastmigrate/core.py +++ b/fastmigrate/core.py @@ -233,7 +233,6 @@ def get_migration_scripts(migrations_dir: Path) -> Dict[int, Path]: f"{migration_scripts[version]} and {file_path}" ) migration_scripts[version] = file_path - return migration_scripts