Skip to content

Commit 94ce146

Browse files
committed
test: add targeted coverage boost tests (99% overall)
1 parent 34785d2 commit 94ce146

1 file changed

Lines changed: 260 additions & 0 deletions

File tree

tests/test_coverage_boost.py

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
from __future__ import annotations
2+
3+
# pyright: reportPrivateUsage=false, reportAny=false, reportExplicitAny=false, reportUnannotatedClassAttribute=false, reportUnusedCallResult=false
4+
5+
from types import SimpleNamespace
6+
from typing import Any, cast
7+
8+
import pytest
9+
from sqlalchemy import Column, Integer, MetaData, String, Table, bindparam, create_engine, select
10+
from sqlalchemy.exc import CompileError, SAWarning
11+
from sqlalchemy.sql.base import ColumnCollection
12+
from sqlalchemy.sql.ddl import ExecutableDDLElement
13+
14+
from sqlalchemy_excel.compiler import ExcelCompiler
15+
from sqlalchemy_excel.ddl import ExcelDDLCompiler
16+
from sqlalchemy_excel.dialect import ExcelDialect, _after_create, _after_drop
17+
from sqlalchemy_excel.dml import OnConflictClause, OnConflictDoNothing, OnConflictDoUpdate, insert
18+
19+
20+
@pytest.fixture
21+
def users_table() -> Table:
22+
metadata = MetaData()
23+
return Table(
24+
"users",
25+
metadata,
26+
Column("id", Integer, primary_key=True),
27+
Column("name", String),
28+
Column("age", Integer),
29+
)
30+
31+
32+
class AddColumnNoColumn(ExecutableDDLElement):
33+
__visit_name__ = "add_column"
34+
35+
def __init__(self, table_name: str) -> None:
36+
self.table_name = table_name
37+
self.schema = None
38+
39+
40+
class DropColumnFromColumnObj(ExecutableDDLElement):
41+
__visit_name__ = "drop_column"
42+
43+
def __init__(self, table_name: str, column: Any) -> None:
44+
self.table_name = table_name
45+
self.column = column
46+
self.schema = None
47+
48+
49+
class RenameColumnAltNames(ExecutableDDLElement):
50+
__visit_name__ = "rename_column"
51+
52+
def __init__(self, table_name: str, old_name: str, new_name: str) -> None:
53+
self.table_name = table_name
54+
self.old_name = old_name
55+
self.new_name = new_name
56+
self.schema = None
57+
58+
59+
def test_compiler_is_true_onclause_none_path() -> None:
60+
assert ExcelCompiler._is_true_onclause(None) is False
61+
62+
63+
def test_compiler_validate_join_tree_requires_on_clause() -> None:
64+
bad_join = SimpleNamespace(left=object(), right=object(), onclause=None, full=False, isouter=False)
65+
with pytest.raises(CompileError, match="requires an ON clause"):
66+
ExcelCompiler._validate_join_tree(cast(Any, bad_join))
67+
68+
69+
def test_compiler_validate_join_tree_recurses_into_right_join() -> None:
70+
metadata = MetaData()
71+
users = Table("users", metadata, Column("id", Integer, primary_key=True))
72+
orders = Table("orders", metadata, Column("user_id", Integer))
73+
items = Table("items", metadata, Column("order_user_id", Integer))
74+
75+
right_join = orders.join(items, orders.c.user_id == items.c.order_user_id)
76+
outer_join = users.join(right_join, users.c.id == users.c.id)
77+
with pytest.raises(CompileError, match="different join sources"):
78+
ExcelCompiler._validate_join_tree(cast(Any, outer_join))
79+
80+
81+
def test_compiler_count_empty_args_rejected(tmp_xlsx: str, users_table: Table) -> None:
82+
from sqlalchemy.sql.functions import Function
83+
84+
engine = create_engine(f"excel:///{tmp_xlsx}")
85+
stmt = select(cast(Any, Function("sum"))).select_from(users_table)
86+
with pytest.raises(CompileError, match="expression arguments"):
87+
stmt.compile(dialect=engine.dialect)
88+
engine.dispose()
89+
90+
91+
def test_compiler_visit_subquery_rejects_when_join_context(tmp_xlsx: str) -> None:
92+
engine = create_engine(f"excel:///{tmp_xlsx}")
93+
metadata = MetaData()
94+
users = Table("users", metadata, Column("id", Integer, primary_key=True))
95+
orders = Table("orders", metadata, Column("user_id", Integer))
96+
sub = select(orders.c.user_id).subquery()
97+
98+
compiler_inst = cast(Any, select(users).compile(dialect=engine.dialect))
99+
compiler_inst._in_in_clause = True
100+
compiler_inst._has_join = True
101+
with pytest.raises(CompileError, match="subqueries with JOIN"):
102+
compiler_inst.visit_subquery(sub)
103+
engine.dispose()
104+
105+
106+
def test_compiler_quote_aware_helpers_cover_escaped_quotes() -> None:
107+
sql = "('a''b')"
108+
depth = ExcelCompiler._update_depth_quote_aware(sql, 0, len(sql), 0)
109+
assert depth == 0
110+
assert ExcelCompiler._has_top_level_compound_op("SELECT 'UNION''ALL' FROM t") is False
111+
112+
113+
def test_compiler_strip_compound_parens_with_quoted_segments() -> None:
114+
sql = "'x''y' (SELECT 'a''b' AS s FROM t1 ORDER BY s) UNION SELECT id FROM t2"
115+
out = ExcelCompiler._strip_compound_branch_parens(sql)
116+
assert "ORDER BY s" in out
117+
assert "'x''y'" in out
118+
119+
120+
def test_compiler_strip_order_and_limit_helpers_with_quoted_and_embedded_keywords() -> None:
121+
stripped = ExcelCompiler._strip_top_level_order_by(
122+
"SELECT id FROM t ORDER BY id, 'LIMIT' LIMITED 5 OFFSET 2"
123+
)
124+
assert stripped.endswith("OFFSET 2")
125+
126+
assert (
127+
ExcelCompiler._has_top_level_limit_offset(
128+
"SELECT 'LIMIT' AS x FROM t WHERE name = 'OFFSET'"
129+
)
130+
is False
131+
)
132+
133+
134+
def test_compiler_on_conflict_do_nothing_without_target_text(tmp_xlsx: str, users_table: Table) -> None:
135+
engine = create_engine(f"excel:///{tmp_xlsx}")
136+
compiler_inst = cast(Any, select(users_table).compile(dialect=engine.dialect))
137+
clause = OnConflictDoNothing(index_elements=None)
138+
assert compiler_inst.visit_on_conflict_do_nothing(clause) == "ON CONFLICT DO NOTHING"
139+
engine.dispose()
140+
141+
142+
def test_compiler_upsert_bindparam_null_type_and_extra_key_warns(
143+
tmp_xlsx: str,
144+
users_table: Table,
145+
) -> None:
146+
engine = create_engine(f"excel:///{tmp_xlsx}")
147+
stmt = insert(users_table).values(id=1, name="a", age=1).on_conflict_do_update(
148+
index_elements=["id"],
149+
set_={"age": bindparam("new_age"), "extra_col": 10},
150+
)
151+
with pytest.warns(SAWarning, match="Additional column names"):
152+
sql = str(stmt.compile(dialect=engine.dialect))
153+
assert "extra_col" in sql
154+
engine.dispose()
155+
156+
157+
def test_ddl_compiler_error_paths_and_alternative_name_sources(tmp_xlsx: str) -> None:
158+
engine = create_engine(f"excel:///{tmp_xlsx}")
159+
compiler = ExcelDDLCompiler(engine.dialect, cast(Any, None))
160+
161+
with pytest.raises(CompileError, match="does not support schemas"):
162+
compiler._format_alter_table_name(SimpleNamespace(schema="s", table_name="users"))
163+
with pytest.raises(CompileError, match="requires a table name"):
164+
compiler._format_alter_table_name(SimpleNamespace(schema=None, table_name=None, table=None))
165+
with pytest.raises(CompileError, match="requires a column name"):
166+
compiler._format_alter_column_name(None)
167+
with pytest.raises(CompileError, match="requires a column"):
168+
AddColumnNoColumn("users").compile(dialect=engine.dialect)
169+
170+
dropped = str(
171+
DropColumnFromColumnObj("users", SimpleNamespace(name="age")).compile(
172+
dialect=engine.dialect
173+
)
174+
)
175+
assert dropped == "ALTER TABLE users DROP COLUMN age"
176+
177+
renamed = str(
178+
RenameColumnAltNames("users", "old_col", "new_col").compile(
179+
dialect=engine.dialect
180+
)
181+
)
182+
assert renamed == "ALTER TABLE users RENAME COLUMN old_col TO new_col"
183+
engine.dispose()
184+
185+
186+
def test_dml_clause_none_targets_and_invalid_set_inputs() -> None:
187+
clause = OnConflictClause(index_elements=None)
188+
assert clause.inferred_target_elements is None
189+
assert clause.inferred_target_whereclause is None
190+
191+
with pytest.raises(ValueError, match="must not be empty"):
192+
OnConflictDoUpdate(index_elements=["id"], set_=ColumnCollection())
193+
194+
195+
def test_dialect_event_hooks_return_early_for_non_excel(users_table: Table) -> None:
196+
fake_conn = SimpleNamespace(dialect=SimpleNamespace(name="sqlite"))
197+
_after_create(users_table, fake_conn)
198+
_after_drop(users_table, fake_conn)
199+
200+
201+
def test_dialect_sync_alter_table_short_statement_returns_early() -> None:
202+
dialect = ExcelDialect()
203+
cursor = SimpleNamespace(connection=object())
204+
dialect._sync_alter_table_metadata(cursor, "ALTER TABLE users")
205+
206+
207+
def test_dialect_sync_alter_table_add_float_maps_to_real(monkeypatch: pytest.MonkeyPatch) -> None:
208+
writes: list[list[dict[str, Any]]] = []
209+
210+
def read_table_metadata(_conn: Any, _table: str) -> list[dict[str, Any]]:
211+
return [{"name": "id", "type_name": "INTEGER", "nullable": False, "primary_key": True}]
212+
213+
def get_columns(_conn: Any, _table: str) -> list[dict[str, Any]]:
214+
return [{"name": "id", "type": "INTEGER"}, {"name": "amount", "type": "FLOAT"}]
215+
216+
def write_table_metadata(_conn: Any, _table: str, columns: list[dict[str, Any]]) -> None:
217+
writes.append(columns)
218+
219+
import excel_dbapi
220+
221+
monkeypatch.setattr(excel_dbapi, "read_table_metadata", read_table_metadata)
222+
monkeypatch.setattr(excel_dbapi, "get_columns", get_columns)
223+
monkeypatch.setattr(excel_dbapi, "write_table_metadata", write_table_metadata)
224+
225+
dialect = ExcelDialect()
226+
cursor = SimpleNamespace(connection=object())
227+
dialect._sync_alter_table_metadata(cursor, "ALTER TABLE users ADD COLUMN amount FLOAT")
228+
229+
amount = next(col for col in writes[-1] if col["name"] == "amount")
230+
assert amount["type_name"] == "REAL"
231+
232+
233+
def test_dialect_sync_alter_table_rename_unknown_column_keeps_maps_stable(
234+
monkeypatch: pytest.MonkeyPatch,
235+
) -> None:
236+
writes: list[list[dict[str, Any]]] = []
237+
238+
def read_table_metadata(_conn: Any, _table: str) -> list[dict[str, Any]]:
239+
return []
240+
241+
def get_columns(_conn: Any, _table: str) -> list[dict[str, Any]]:
242+
return [{"name": "new_name", "type": "TEXT"}]
243+
244+
def write_table_metadata(_conn: Any, _table: str, columns: list[dict[str, Any]]) -> None:
245+
writes.append(columns)
246+
247+
import excel_dbapi
248+
249+
monkeypatch.setattr(excel_dbapi, "read_table_metadata", read_table_metadata)
250+
monkeypatch.setattr(excel_dbapi, "get_columns", get_columns)
251+
monkeypatch.setattr(excel_dbapi, "write_table_metadata", write_table_metadata)
252+
253+
dialect = ExcelDialect()
254+
cursor = SimpleNamespace(connection=object())
255+
dialect._sync_alter_table_metadata(
256+
cursor,
257+
"ALTER TABLE users RENAME COLUMN old_name TO new_name",
258+
)
259+
260+
assert writes[-1] == [{"name": "new_name", "type_name": "TEXT", "nullable": True, "primary_key": False}]

0 commit comments

Comments
 (0)