Skip to content
Merged
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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
14 changes: 9 additions & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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"
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ Thumbs.db
*.sqlite

# Backups
*.bak
*.bak
.env
15 changes: 6 additions & 9 deletions src/core/schema_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

"""Shared test fixtures and configuration"""

import os
import pytest
import psycopg
import sys
Expand All @@ -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")
Expand Down
34 changes: 34 additions & 0 deletions tests/test_schema_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading