Skip to content

Commit 653e901

Browse files
Coding-Dev-ToolsDevForge Engineer
andauthored
test: add standalone dialect tests and converter edge cases (#10)
- Create tests/test_dialects.py with 48 tests covering: - Dialect enum values and membership - sql_type_for() for all types across PostgreSQL, MySQL, SQLite - quote_identifier() for all dialects (backtick vs double-quote) - format_value() edge cases: single quotes, booleans per dialect, None - create_table_sql() and insert_sql() including multi-row VALUES - Add converter edge case tests to test_converter.py: - Unsupported JSON root type (string, number, boolean) -> ValueError - generate_schema with flatten=True and nested arrays - generate_schema with primitive arrays - generate_schema with single object at root Coverage: 89% -> 96% (230 stmts, 10 uncovered, all optional license paths) Co-authored-by: DevForge Engineer <engineer@devforge.dev>
1 parent 8207dd4 commit 653e901

2 files changed

Lines changed: 309 additions & 0 deletions

File tree

tests/test_converter.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,58 @@ def test_missing_keys_across_rows(self, converter_postgres):
158158
result = converter_postgres.convert(data, table_name="users")
159159
assert "email" in result
160160
assert "age" in result
161+
162+
def test_unsupported_root_type_raises(self, converter_postgres):
163+
"""A plain string at root raises ValueError."""
164+
with pytest.raises(ValueError, match="Unsupported JSON root type"):
165+
converter_postgres.convert('"just a string"', table_name="bad")
166+
167+
def test_unsupported_root_number_raises(self, converter_postgres):
168+
"""A plain number at root raises ValueError."""
169+
with pytest.raises(ValueError, match="Unsupported JSON root type"):
170+
converter_postgres.convert("42", table_name="bad")
171+
172+
def test_unsupported_root_bool_raises(self, converter_postgres):
173+
"""A plain boolean at root raises ValueError."""
174+
with pytest.raises(ValueError, match="Unsupported JSON root type"):
175+
converter_postgres.convert("true", table_name="bad")
176+
177+
178+
class TestGenerateSchema:
179+
"""Tests for generate_schema method."""
180+
181+
def test_generate_schema_basic(self, converter_postgres):
182+
data = json.dumps([{"name": "Alice", "age": 30}])
183+
result = converter_postgres.generate_schema(data, table_name="users")
184+
assert "CREATE TABLE" in result
185+
assert "INSERT INTO" not in result
186+
187+
def test_generate_schema_single_object(self, converter_postgres):
188+
"""Single dict at root also works."""
189+
data = json.dumps({"name": "Alice"})
190+
result = converter_postgres.generate_schema(data, table_name="users")
191+
assert "CREATE TABLE" in result
192+
assert '"name"' in result
193+
194+
def test_generate_schema_with_flatten_nested_array(self):
195+
"""generate_schema with flatten=True should produce CREATE TABLE for nested arrays."""
196+
conv = JSONToSQLConverter(dialect=Dialect.POSTGRES, flatten=True)
197+
data = json.dumps({
198+
"id": 1,
199+
"name": "Alice",
200+
"orders": [
201+
{"product": "Widget", "qty": 3},
202+
{"product": "Gadget", "qty": 1},
203+
],
204+
})
205+
result = conv.generate_schema(data, table_name="users")
206+
assert "CREATE TABLE" in result
207+
# Should include the nested table schema
208+
assert "users_orders" in result or "orders" in result
209+
assert "INSERT INTO" not in result
210+
211+
def test_generate_schema_primitives(self, converter_postgres):
212+
"""A primitive type should produce a simple schema."""
213+
data = json.dumps([1, 2, 3])
214+
result = converter_postgres.generate_schema(data, table_name="nums")
215+
assert "CREATE TABLE" in result

tests/test_dialects.py

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
"""Standalone tests for json2sql dialects module."""
2+
import pytest
3+
from json2sql.dialects import (
4+
Dialect,
5+
sql_type_for,
6+
quote_identifier,
7+
format_value,
8+
create_table_sql,
9+
insert_sql,
10+
)
11+
12+
13+
# --- Dialect enum ---
14+
15+
class TestDialectEnum:
16+
"""Dialect enum values and membership."""
17+
18+
def test_postgres_value(self):
19+
assert Dialect.POSTGRES == "postgres"
20+
21+
def test_mysql_value(self):
22+
assert Dialect.MYSQL == "mysql"
23+
24+
def test_sqlite_value(self):
25+
assert Dialect.SQLITE == "sqlite"
26+
27+
def test_all_members(self):
28+
assert set(Dialect) == {Dialect.POSTGRES, Dialect.MYSQL, Dialect.SQLITE}
29+
30+
31+
# --- sql_type_for ---
32+
33+
class TestSQLTypeFor:
34+
"""Python type → SQL type mapping per dialect."""
35+
36+
@pytest.mark.parametrize("dialect", [Dialect.POSTGRES, Dialect.MYSQL, Dialect.SQLITE])
37+
def test_string_type(self, dialect):
38+
assert "TEXT" in sql_type_for("hello", dialect).upper() or "VARCHAR" in sql_type_for("hello", dialect).upper()
39+
40+
def test_postgres_int(self):
41+
assert sql_type_for(42, Dialect.POSTGRES) == "INTEGER"
42+
43+
def test_mysql_int(self):
44+
assert sql_type_for(42, Dialect.MYSQL) == "INT"
45+
46+
def test_sqlite_int(self):
47+
assert sql_type_for(42, Dialect.SQLITE) == "INTEGER"
48+
49+
def test_postgres_float(self):
50+
assert sql_type_for(3.14, Dialect.POSTGRES) == "DOUBLE PRECISION"
51+
52+
def test_mysql_float(self):
53+
assert sql_type_for(3.14, Dialect.MYSQL) == "DOUBLE"
54+
55+
def test_sqlite_float(self):
56+
assert sql_type_for(3.14, Dialect.SQLITE) == "REAL"
57+
58+
def test_postgres_bool(self):
59+
assert sql_type_for(True, Dialect.POSTGRES) == "BOOLEAN"
60+
61+
def test_mysql_bool(self):
62+
assert sql_type_for(False, Dialect.MYSQL) == "TINYINT(1)"
63+
64+
def test_sqlite_bool(self):
65+
assert sql_type_for(True, Dialect.SQLITE) == "INTEGER"
66+
67+
def test_none_type(self):
68+
# None maps to the string type of each dialect
69+
for dialect in Dialect:
70+
result = sql_type_for(None, dialect)
71+
assert isinstance(result, str)
72+
assert len(result) > 0
73+
74+
def test_unknown_type(self):
75+
# Unrecognized type falls back to TEXT
76+
assert sql_type_for(b"bytes", Dialect.POSTGRES) == "TEXT"
77+
78+
def test_bool_before_int_check(self):
79+
# bool is subclass of int; must return bool type, not int
80+
assert sql_type_for(True, Dialect.POSTGRES) == "BOOLEAN"
81+
assert sql_type_for(True, Dialect.POSTGRES) != "INTEGER"
82+
83+
84+
# --- quote_identifier ---
85+
86+
class TestQuoteIdentifier:
87+
"""Identifier quoting per dialect."""
88+
89+
def test_mysql_backtick(self):
90+
assert quote_identifier("users", Dialect.MYSQL) == "`users`"
91+
92+
def test_postgres_double_quote(self):
93+
assert quote_identifier("users", Dialect.POSTGRES) == '"users"'
94+
95+
def test_sqlite_double_quote(self):
96+
assert quote_identifier("users", Dialect.SQLITE) == '"users"'
97+
98+
def test_special_chars_mysql(self):
99+
assert quote_identifier("group", Dialect.MYSQL) == "`group`"
100+
101+
def test_special_chars_postgres(self):
102+
assert quote_identifier("group", Dialect.POSTGRES) == '"group"'
103+
104+
def test_empty_name(self):
105+
for dialect in Dialect:
106+
result = quote_identifier("", dialect)
107+
assert len(result) >= 2
108+
109+
110+
# --- format_value ---
111+
112+
class TestFormatValue:
113+
"""Python value → SQL literal formatting."""
114+
115+
def test_null(self):
116+
for dialect in Dialect:
117+
assert format_value(None, dialect) == "NULL"
118+
119+
def test_postgres_true(self):
120+
assert format_value(True, Dialect.POSTGRES) == "TRUE"
121+
122+
def test_postgres_false(self):
123+
assert format_value(False, Dialect.POSTGRES) == "FALSE"
124+
125+
def test_mysql_true(self):
126+
assert format_value(True, Dialect.MYSQL) == "1"
127+
128+
def test_mysql_false(self):
129+
assert format_value(False, Dialect.MYSQL) == "0"
130+
131+
def test_sqlite_true(self):
132+
assert format_value(True, Dialect.SQLITE) == "1"
133+
134+
def test_sqlite_false(self):
135+
assert format_value(False, Dialect.SQLITE) == "0"
136+
137+
def test_string_simple(self):
138+
assert format_value("hello", Dialect.POSTGRES) == "'hello'"
139+
140+
def test_string_with_single_quote(self):
141+
assert format_value("it's a test", Dialect.POSTGRES) == "'it''s a test'"
142+
143+
def test_string_with_double_quotes(self):
144+
assert format_value('say "hi"', Dialect.POSTGRES) == """'say "hi"'"""
145+
146+
def test_integer(self):
147+
assert format_value(42, Dialect.POSTGRES) == "42"
148+
149+
def test_negative_integer(self):
150+
assert format_value(-5, Dialect.MYSQL) == "-5"
151+
152+
def test_float(self):
153+
assert format_value(3.14, Dialect.POSTGRES) == "3.14"
154+
155+
def test_zero(self):
156+
assert format_value(0, Dialect.SQLITE) == "0"
157+
158+
def test_empty_string(self):
159+
assert format_value("", Dialect.POSTGRES) == "''"
160+
161+
162+
# --- create_table_sql ---
163+
164+
class TestCreateTableSQL:
165+
"""CREATE TABLE statement generation."""
166+
167+
def test_single_column(self):
168+
columns = {"name": "TEXT"}
169+
sql = create_table_sql("users", columns, Dialect.POSTGRES)
170+
assert sql.startswith("CREATE TABLE")
171+
assert '"users"' in sql
172+
assert '"name" TEXT' in sql
173+
assert sql.endswith(";")
174+
175+
def test_multiple_columns(self):
176+
columns = {"id": "INTEGER", "name": "TEXT", "active": "BOOLEAN"}
177+
sql = create_table_sql("users", columns, Dialect.POSTGRES)
178+
assert sql.count('"') >= 6 # table + 3 columns each double-quoted
179+
assert "INTEGER" in sql
180+
assert "TEXT" in sql
181+
assert "BOOLEAN" in sql
182+
183+
def test_mysql_backtick_quoting(self):
184+
columns = {"id": "INT"}
185+
sql = create_table_sql("users", columns, Dialect.MYSQL)
186+
assert "`users`" in sql
187+
assert "`id`" in sql
188+
189+
def test_sqlite(self):
190+
columns = {"value": "TEXT"}
191+
sql = create_table_sql("data", columns, Dialect.SQLITE)
192+
assert '"data"' in sql
193+
assert '"value" TEXT' in sql
194+
195+
196+
# --- insert_sql ---
197+
198+
class TestInsertSQL:
199+
"""INSERT statement generation."""
200+
201+
def test_postgres_single_row(self):
202+
sql = insert_sql("users", ["name"], [["'Alice'"]], Dialect.POSTGRES)
203+
assert sql.startswith("INSERT INTO")
204+
assert '"users"' in sql
205+
assert "'Alice'" in sql
206+
207+
def test_postgres_multi_row(self):
208+
"""PostgreSQL uses multi-row VALUES syntax for >1 rows."""
209+
rows = [["'Alice'"], ["'Bob'"]]
210+
sql = insert_sql("users", ["name"], rows, Dialect.POSTGRES)
211+
assert sql.startswith("INSERT INTO")
212+
assert "VALUES" in sql
213+
# Should be a single statement with two value tuples
214+
assert sql.count("VALUES") == 1
215+
assert "'Alice'" in sql
216+
assert "'Bob'" in sql
217+
218+
def test_mysql_multi_row(self):
219+
"""MySQL multi-row VALUES."""
220+
rows = [["1"], ["2"]]
221+
sql = insert_sql("items", ["id"], rows, Dialect.MYSQL)
222+
assert "VALUES" in sql
223+
assert "1" in sql
224+
assert "2" in sql
225+
assert sql.count("VALUES") == 1
226+
227+
def test_sqlite_single_row_per_statement(self):
228+
"""SQLite uses one INSERT per row (multi-row not supported)."""
229+
rows = [["'Alice'"], ["'Bob'"]]
230+
sql = insert_sql("users", ["name"], rows, Dialect.SQLITE)
231+
# SQLite generates individual INSERT statements
232+
assert sql.count("INSERT INTO") == 2
233+
assert "VALUES" in sql
234+
assert "'Alice'" in sql
235+
assert "'Bob'" in sql
236+
237+
def test_sqlite_single_row(self):
238+
sql = insert_sql("users", ["name"], [["'Alice'"]], Dialect.SQLITE)
239+
assert sql.startswith("INSERT INTO")
240+
assert sql.count("INSERT INTO") == 1
241+
242+
def test_multi_column_postgres(self):
243+
rows = [["1", "'Alice'"], ["2", "'Bob'"]]
244+
sql = insert_sql("users", ["id", "name"], rows, Dialect.POSTGRES)
245+
assert '"id"' in sql
246+
assert '"name"' in sql
247+
assert "'Alice'" in sql
248+
assert "'Bob'" in sql
249+
250+
def test_mysql_quoting(self):
251+
rows = [["1"]]
252+
sql = insert_sql("items", ["id"], rows, Dialect.MYSQL)
253+
assert "`items`" in sql
254+
assert "`id`" in sql

0 commit comments

Comments
 (0)