|
| 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