Skip to content

Commit 3cf819e

Browse files
committed
test: Add integration tests for PostgreSQL schema discovery
Exercises SchemaDiscoveryService against a real PostgreSQL 16 instance via testcontainers, following the PostgresContainer pattern already used elsewhere in the integration suite. Covers schema listing with system namespaces excluded, table and view enumeration, column type mapping across the common PostgreSQL types, primary-key and nullability reporting, describing a view, and the not-found and connection-failure paths. Includes a regression guard asserting that no column in the fixture table maps to UNKNOWN. That assertion fails if column types ever start coming from SQLAlchemy's type spellings ("VARCHAR(255)") rather than the catalog spellings the Feast type mappers expect. These tests require Docker and are marked with pytest.mark.integration. Signed-off-by: Chaitany Patel <patelchaitany93@gmail.com>
1 parent 65e8822 commit 3cf819e

2 files changed

Lines changed: 178 additions & 0 deletions

File tree

sdk/python/tests/integration/schema_discovery/__init__.py

Whitespace-only changes.
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
"""End-to-end schema discovery against a real PostgreSQL instance."""
2+
3+
import pytest
4+
from testcontainers.postgres import PostgresContainer
5+
6+
from feast.schema_discovery import SchemaDiscoveryService
7+
from feast.schema_discovery.errors import (
8+
DatabaseConnectionError,
9+
SchemaNotFoundError,
10+
TableNotFoundError,
11+
)
12+
13+
DDL = """
14+
CREATE TABLE public.users (
15+
user_id BIGINT PRIMARY KEY,
16+
email VARCHAR(255),
17+
age INTEGER NOT NULL,
18+
balance DOUBLE PRECISION,
19+
is_active BOOLEAN NOT NULL,
20+
created_at TIMESTAMP NOT NULL,
21+
metadata JSONB
22+
);
23+
24+
CREATE VIEW public.active_users AS
25+
SELECT user_id, email FROM public.users WHERE is_active;
26+
27+
CREATE SCHEMA analytics;
28+
CREATE TABLE analytics.daily_metrics (
29+
metric_date DATE PRIMARY KEY,
30+
value NUMERIC
31+
);
32+
"""
33+
34+
35+
@pytest.fixture(scope="module")
36+
def postgres_url():
37+
with PostgresContainer(
38+
"postgres:16",
39+
username="root",
40+
password="test!@#$%", # pragma: allowlist secret
41+
dbname="test",
42+
).with_exposed_ports(5432) as container:
43+
import psycopg
44+
45+
host = container.get_container_host_ip()
46+
port = container.get_exposed_port(5432)
47+
url = ( # pragma: allowlist secret
48+
f"postgresql://root:test!%40%23%24%25@{host}:{port}/test?sslmode=disable"
49+
)
50+
51+
with psycopg.connect(
52+
host=host,
53+
port=port,
54+
user="root",
55+
password="test!@#$%", # pragma: allowlist secret
56+
dbname="test",
57+
sslmode="disable",
58+
) as conn:
59+
conn.execute(DDL)
60+
conn.commit()
61+
62+
yield url
63+
64+
65+
@pytest.fixture
66+
def service():
67+
return SchemaDiscoveryService(connect_timeout=10)
68+
69+
70+
@pytest.mark.integration
71+
class TestListSchemas:
72+
def test_lists_user_schemas_only(self, service, postgres_url):
73+
result = service.list_schemas(postgres_url, include_tables=False)
74+
names = {s.name for s in result.schemas}
75+
assert {"public", "analytics"} <= names
76+
assert not any(n.startswith("pg_") for n in names)
77+
assert "information_schema" not in names
78+
79+
def test_reports_database_and_dialect(self, service, postgres_url):
80+
result = service.list_schemas(postgres_url, include_tables=False)
81+
assert result.database == "test"
82+
assert result.dialect == "postgresql"
83+
84+
def test_includes_tables_and_views(self, service, postgres_url):
85+
result = service.list_schemas(postgres_url, include_tables=True)
86+
public = next(s for s in result.schemas if s.name == "public")
87+
by_name = {t.name: t.type for t in public.tables}
88+
assert by_name["users"] == "table"
89+
assert by_name["active_users"] == "view"
90+
91+
def test_scopes_to_one_schema(self, service, postgres_url):
92+
result = service.list_schemas(postgres_url, schema="analytics")
93+
assert [s.name for s in result.schemas] == ["analytics"]
94+
assert [t.name for t in result.schemas[0].tables] == ["daily_metrics"]
95+
96+
def test_unknown_schema_raises(self, service, postgres_url):
97+
with pytest.raises(SchemaNotFoundError):
98+
service.list_schemas(postgres_url, schema="does_not_exist")
99+
100+
101+
@pytest.mark.integration
102+
class TestListTables:
103+
def test_lists_tables_in_schema(self, service, postgres_url):
104+
tables = service.list_tables(postgres_url, "analytics")
105+
assert [t.name for t in tables] == ["daily_metrics"]
106+
107+
def test_unknown_schema_raises(self, service, postgres_url):
108+
with pytest.raises(SchemaNotFoundError):
109+
service.list_tables(postgres_url, "does_not_exist")
110+
111+
112+
@pytest.mark.integration
113+
class TestDescribeTable:
114+
def test_maps_every_column_to_a_feast_type(self, service, postgres_url):
115+
result = service.describe_table(postgres_url, "public.users")
116+
by_name = {c.name: c for c in result.columns}
117+
118+
assert by_name["user_id"].feast_type == "INT64"
119+
assert by_name["email"].feast_type == "STRING"
120+
assert by_name["age"].feast_type == "INT32"
121+
assert by_name["balance"].feast_type == "DOUBLE"
122+
assert by_name["is_active"].feast_type == "BOOL"
123+
assert by_name["created_at"].feast_type == "UNIX_TIMESTAMP"
124+
assert by_name["metadata"].feast_type == "MAP"
125+
126+
def test_no_column_falls_back_to_unknown(self, service, postgres_url):
127+
"""Regression guard: SQLAlchemy type spellings would map to UNKNOWN."""
128+
result = service.describe_table(postgres_url, "public.users")
129+
assert all(c.feast_type != "UNKNOWN" for c in result.columns)
130+
131+
def test_reports_primary_key(self, service, postgres_url):
132+
result = service.describe_table(postgres_url, "public.users")
133+
pks = {c.name for c in result.columns if c.primary_key}
134+
assert pks == {"user_id"}
135+
136+
def test_reports_nullability(self, service, postgres_url):
137+
by_name = {
138+
c.name: c
139+
for c in service.describe_table(postgres_url, "public.users").columns
140+
}
141+
assert by_name["email"].nullable is True
142+
assert by_name["age"].nullable is False
143+
assert by_name["user_id"].nullable is False
144+
145+
def test_unqualified_table_uses_public(self, service, postgres_url):
146+
result = service.describe_table(postgres_url, "users")
147+
assert result.table == "public.users"
148+
149+
def test_describes_table_in_non_default_schema(self, service, postgres_url):
150+
result = service.describe_table(postgres_url, "analytics.daily_metrics")
151+
assert {c.name for c in result.columns} == {"metric_date", "value"}
152+
153+
def test_schema_argument_qualifies_table(self, service, postgres_url):
154+
result = service.describe_table(
155+
postgres_url, "daily_metrics", schema="analytics"
156+
)
157+
assert result.table == "analytics.daily_metrics"
158+
159+
def test_describes_a_view(self, service, postgres_url):
160+
result = service.describe_table(postgres_url, "public.active_users")
161+
assert [c.name for c in result.columns] == ["user_id", "email"]
162+
assert all(c.primary_key is False for c in result.columns)
163+
164+
def test_unknown_table_raises(self, service, postgres_url):
165+
with pytest.raises(TableNotFoundError):
166+
service.describe_table(postgres_url, "public.does_not_exist")
167+
168+
def test_unknown_schema_raises(self, service, postgres_url):
169+
with pytest.raises(TableNotFoundError):
170+
service.describe_table(postgres_url, "nosuchschema.users")
171+
172+
173+
@pytest.mark.integration
174+
class TestConnectionFailures:
175+
def test_bad_credentials_raise_connection_error(self, service, postgres_url):
176+
broken = postgres_url.replace("root:", "wronguser:")
177+
with pytest.raises(DatabaseConnectionError):
178+
service.list_schemas(broken)

0 commit comments

Comments
 (0)