diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e1bba19 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +# Copy to .env and customize if you need non-default test DB settings. +TEST_DB=sql_schema_studio_test +TEST_USER=postgres +TEST_PASSWORD=admin123 +TEST_HOST=localhost +TEST_PORT=5432 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cc172b1..88e61f7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,17 +15,15 @@ jobs: matrix: python-version: ["3.12", "3.13"] - # PostgreSQL service container for database tests services: postgres: image: postgres:16 env: POSTGRES_DB: postgres POSTGRES_USER: postgres - POSTGRES_PASSWORD: admin123 # Matches TEST_PASSWORD in conftest.py + POSTGRES_PASSWORD: ${{ secrets.TEST_DB_PASSWORD }} ports: - 5432:5432 - # Ensure PostgreSQL is ready before steps start options: >- --health-cmd pg_isready --health-interval 10s @@ -66,9 +64,9 @@ jobs: run: | python -m pip install --upgrade pip pip install -e ".[dev]" - pip install flake8-pyproject keyrings.alt + pip install flake8-pyproject keyrings.alt - - name: Run flake8 (optional) + - name: Run flake8 run: | flake8 src/ --count --statistics || true @@ -83,3 +81,9 @@ jobs: - name: Run pytest run: | pytest tests/ -v --tb=short + env: + TEST_PASSWORD: ${{ secrets.TEST_DB_PASSWORD }} + TEST_USER: postgres + TEST_DB: sql_schema_studio_test + TEST_HOST: localhost + TEST_PORT: "5432" diff --git a/.gitignore b/.gitignore index 07ee637..6d00196 100755 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ Thumbs.db *.sqlite # Backups -*.bak \ No newline at end of file +*.bak +.env diff --git a/src/core/schema_parser.py b/src/core/schema_parser.py index 432f061..470b0bd 100644 --- a/src/core/schema_parser.py +++ b/src/core/schema_parser.py @@ -14,6 +14,9 @@ logger = get_logger(__name__) +# A line that starts with one of these is a table constraint, not a column. +CONSTRAINT_LINE_RE = re.compile(r"(PRIMARY\s+KEY|FOREIGN\s+KEY|CONSTRAINT|UNIQUE|CHECK|EXCLUDE)\b") + class SchemaParser: """Parse CREATE TABLE and ALTER TABLE statements from SQL text. @@ -193,15 +196,9 @@ def _parse_column_line_robust(self, line: str) -> dict | None: # Skip pure constraint lines upper = line.upper().strip() - skip_patterns = ( - "PRIMARY KEY", - "FOREIGN KEY", - "CONSTRAINT", - "UNIQUE", - "CHECK", - "EXCLUDE", - ) - if upper.startswith(skip_patterns) and "(" in upper: + # Anchored on a word boundary so a column named unique_code or + # constraint_name is not mistaken for a constraint declaration. + if CONSTRAINT_LINE_RE.match(upper) and "(" in upper: return None # Try to extract column name and type diff --git a/tests/conftest.py b/tests/conftest.py index f172707..3d33e7c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,7 @@ """Shared test fixtures and configuration""" +import os import pytest import psycopg import sys @@ -19,11 +20,11 @@ from src.core.db_connector import DatabaseConnector, ConnectionProfile # Test database settings — use a dedicated test database -TEST_DB = "sql_schema_studio_test" -TEST_USER = "postgres" -TEST_PASSWORD = "admin123" -TEST_HOST = "localhost" -TEST_PORT = 5432 +TEST_DB = os.environ.get("TEST_DB", "sql_schema_studio_test") +TEST_USER = os.environ.get("TEST_USER", "postgres") +TEST_PASSWORD = os.environ.get("TEST_PASSWORD", "admin123") +TEST_HOST = os.environ.get("TEST_HOST", "localhost") +TEST_PORT = int(os.environ.get("TEST_PORT", "5432")) @pytest.fixture(scope="session") diff --git a/tests/test_schema_parser.py b/tests/test_schema_parser.py index df4a0ab..9f8b5b8 100644 --- a/tests/test_schema_parser.py +++ b/tests/test_schema_parser.py @@ -190,6 +190,40 @@ def test_table_level_primary_key(self, parser): assert len(pks) == 2 assert {c["name"] for c in pks} == {"a", "b"} + def test_column_named_like_constraint_keyword(self, parser): + """A column whose name starts with a constraint keyword is a column. + + unique_code and constraint_name used to be dropped because the + constraint check compared raw prefixes. The designer then showed a + table with fewer columns than the SQL declares. + """ + sql = """ + CREATE TABLE t ( + id SERIAL PRIMARY KEY, + unique_code VARCHAR(10), + constraint_name VARCHAR(50), + check_flag BOOLEAN, + excluded_at TIMESTAMP + ); + """ + tables, _ = parser.parse_sql(sql) + names = [c["name"] for c in tables[0]["columns"]] + assert names == ["id", "unique_code", "constraint_name", "check_flag", "excluded_at"] + + def test_table_level_unique_constraint_still_skipped(self, parser): + """The keyword fix must not start parsing real constraint lines.""" + sql = """ + CREATE TABLE t ( + id SERIAL PRIMARY KEY, + email VARCHAR(200), + UNIQUE (email), + CONSTRAINT chk_len CHECK (length(email) > 3) + ); + """ + tables, _ = parser.parse_sql(sql) + names = [c["name"] for c in tables[0]["columns"]] + assert names == ["id", "email"] + def test_not_null(self, parser): sql = "CREATE TABLE t (name VARCHAR(100) NOT NULL);" tables, _ = parser.parse_sql(sql)