# -*- coding: utf-8 -*- """Guards for the in-place column migration. ``create_all`` creates missing tables but never adds columns to a table that already exists, so new model columns are applied by ``_ensure_columns``. That step was originally driven by a hand-kept list, and forgetting to update it did not fail loudly -- the app still started, connected, and then failed on every query and every scheduler tick. These tests pin down its replacement, which derives the work from the model metadata. """ import pytest from sqlalchemy import Boolean, Column, Integer, MetaData, String, Table from api.monitor import db from api.monitor.models import MonitorBase def _migratable_columns(): for table in MonitorBase.metadata.sorted_tables: for column in table.columns: if column.primary_key: continue yield pytest.param(column, id=f"{table.name}.{column.name}") def _ddl(column: Column) -> str: """Render a detached column, so the tests never mutate the real metadata.""" scratch = Table("scratch", MetaData(), column) return db._column_ddl(scratch.columns[0]) @pytest.mark.parametrize("column", _migratable_columns()) def test_column_renders_as_ddl(column): ddl = db._column_ddl(column) # SQLAlchemy back-quotes an identifier only when it has to, so the rendered # name matches the column's with the quoting stripped -- which is the case for # reserved words like monitor_run.trigger. That quoting is a feature: the # hand-kept list this replaced would have emitted bare `trigger` and died on a # syntax error. assert ddl.split(" ", 1)[0].strip("`") == column.name # MySQL refuses AUTO_INCREMENT together with the DEFAULT this helper appends # to NOT NULL columns. assert "AUTO_INCREMENT" not in ddl @pytest.mark.parametrize("column", _migratable_columns()) def test_not_null_columns_carry_a_default(column): """So ADD COLUMN cannot fail on a table that already holds rows. Without a DEFAULT, whether the ALTER succeeds depends on the server's sql_mode -- not something a deployment should hinge on. """ if column.nullable: pytest.skip("nullable column needs no seed value") assert "DEFAULT" in db._column_ddl(column) def test_boolean_default_becomes_a_mysql_literal(): """Python's True is not a SQL keyword; it has to become 1.""" assert "DEFAULT 1" in _ddl(Column("flag", Boolean, nullable=False, default=True)) def test_string_defaults_are_quoted(): assert "DEFAULT 'interval'" in _ddl( Column("mode", String(16), nullable=False, default="interval") ) def test_integer_defaults_are_not_quoted(): ddl = _ddl(Column("n", Integer, nullable=False, default=0)) assert "DEFAULT 0" in ddl assert "DEFAULT '0'" not in ddl def test_a_not_null_column_without_a_model_default_falls_back_to_zero(): """Belt and braces: even a column the model gives no default still migrates.""" ddl = _ddl(Column("n", Integer, nullable=False)) assert "DEFAULT 0" in ddl