diff --git a/src/db/migrate.py b/src/db/migrate.py index 52981447..5f9f8049 100644 --- a/src/db/migrate.py +++ b/src/db/migrate.py @@ -1,10 +1,17 @@ import os +import re + import bcrypt import importlib from pyinfraboxutils import get_logger, get_env from pyinfraboxutils.db import connect_db + +# Matches C-style block comments (/* ... */), across multiple lines. +# Non-greedy so consecutive `/* a */ x /* b */` blocks are matched independently. +_BLOCK_COMMENT_RE = re.compile(r'/\*.*?\*/', re.DOTALL) + logger = get_logger("migrate") def get_files(current_schema_version): @@ -14,10 +21,40 @@ def get_files(current_schema_version): files = [f for f in os.listdir(migration_path) if os.path.isfile(os.path.join(migration_path, f)) and f.startswith('0')] files.sort(key=lambda f: int(f[:5])) - files = files[current_schema_version:] + files = [f for f in files if int(f[:5]) > current_schema_version] return [(os.path.join(migration_path, f), int(f[:5])) for f in files] +def _has_executable_sql(sql): + """Return True iff `sql` contains at least one non-comment, non-blank line. + + `str.strip()` only removes leading/trailing whitespace; SQL comments are + kept. Passing a comment-only string to `cur.execute()` raises + `psycopg2.ProgrammingError: can't execute an empty query`, which breaks + migration runs whenever a placeholder file (e.g. an intentionally empty + migration to close a numbering gap) is applied. This helper detects such + files so the caller can skip the execute call. + + Recognised as "no SQL": + - completely empty / whitespace-only files + - only `-- ...` line comments + - only `/* ... */` block comments (even across lines) + - any combination of the above + + Lightweight heuristic — does not tokenise SQL, so quoted strings that + happen to contain `--` or `/* */` are still treated as executable + (which is fine, they contain real statements). + """ + # Strip C-style block comments first so any `/* ... */`-only content is + # collapsed away before the per-line `--` check. + sql_no_block = _BLOCK_COMMENT_RE.sub('', sql) + for line in sql_no_block.splitlines(): + stripped = line.strip() + if stripped and not stripped.startswith('--'): + return True + return False + + def apply_migration(conn, migration): filename = migration[0] logger.info("Starting to apply migration %s", filename) @@ -27,8 +64,13 @@ def apply_migration(conn, migration): sql = sql_file.read().strip() cur = conn.cursor() - if sql: + if _has_executable_sql(sql): cur.execute(sql) + else: + logger.info( + "Skipping execute for %s (no non-comment SQL statements)", + filename, + ) cur.close() elif filename.endswith('.py'):