diff --git a/sql_drills/pyproject.toml b/sql_drills/pyproject.toml index 3eec6e1..8426301 100644 --- a/sql_drills/pyproject.toml +++ b/sql_drills/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sql_drills" -version = "0.2.0" +version = "0.2.1" description = "SQL drills (MySQL, PostgreSQL): sandboxed grading, LeetCode statement examples" requires-python = ">=3.14" # Stdlib only on purpose: the tutor server imports it from the system python. @@ -144,7 +144,10 @@ generated-members = [ exclude_dirs = ["tests", ".venv"] [tool.pytest.ini_options] -markers = ["docker: needs a live sql-drills MySQL or PostgreSQL container (skipped when it is down)"] +markers = [ + "docker: needs a live sql-drills MySQL or PostgreSQL container (skipped when it is down)", + "network: fetches a real LeetCode statement at runtime (skipped when unavailable)", +] testpaths = ["sql_drills/tests"] python_files = ["test_*.py", "*_test.py"] python_classes = ["Test*"] diff --git a/sql_drills/sql_drills/tests/statements/monthly-transactions-i.txt b/sql_drills/sql_drills/tests/statements/monthly-transactions-i.txt deleted file mode 100644 index e59b431..0000000 --- a/sql_drills/sql_drills/tests/statements/monthly-transactions-i.txt +++ /dev/null @@ -1,62 +0,0 @@ -# 1193. Monthly Transactions I (Medium) -Tags: Database -Languages available: mysql, mssql, postgresql, oraclesql, pythondata - -Table: `Transactions` - -``` - -+---------------+---------+ -| Column Name | Type | -+---------------+---------+ -| id | int | -| country | varchar | -| state | enum | -| amount | int | -| trans_date | date | -+---------------+---------+ -id is the primary key of this table. -The table has information about incoming transactions. -The state column is an enum of type ["approved", "declined"]. - -``` - - - -Write an SQL query to find for each month and country, the number of transactions and their total amount, the number of approved transactions and their total amount. - -Return the result table in **any order**. - -The query result format is in the following example. - - - -**Example 1:** - -``` - -**Input:** -Transactions table: -+------+---------+----------+--------+------------+ -| id | country | state | amount | trans_date | -+------+---------+----------+--------+------------+ -| 121 | US | approved | 1000 | 2018-12-18 | -| 122 | US | declined | 2000 | 2018-12-19 | -| 123 | US | approved | 2000 | 2019-01-01 | -| 124 | DE | approved | 2000 | 2019-01-07 | -+------+---------+----------+--------+------------+ -**Output:** -+----------+---------+-------------+----------------+--------------------+-----------------------+ -| month | country | trans_count | approved_count | trans_total_amount | approved_total_amount | -+----------+---------+-------------+----------------+--------------------+-----------------------+ -| 2018-12 | US | 2 | 1 | 3000 | 1000 | -| 2019-01 | US | 1 | 1 | 2000 | 2000 | -| 2019-01 | DE | 1 | 1 | 2000 | 2000 | -+----------+---------+-------------+----------------+--------------------+-----------------------+ - -``` - -## Starter code (mysql) -``` -# Write your MySQL query statement below -``` diff --git a/sql_drills/sql_drills/tests/statements/reformat-department-table.txt b/sql_drills/sql_drills/tests/statements/reformat-department-table.txt deleted file mode 100644 index 8b9992f..0000000 --- a/sql_drills/sql_drills/tests/statements/reformat-department-table.txt +++ /dev/null @@ -1,63 +0,0 @@ -# 1179. Reformat Department Table (Easy) -Tags: Database -Languages available: mysql, mssql, postgresql, oraclesql, pythondata - -Table: `Department` - -``` - -+-------------+---------+ -| Column Name | Type | -+-------------+---------+ -| id | int | -| revenue | int | -| month | varchar | -+-------------+---------+ -In SQL,(id, month) is the primary key of this table. -The table has information about the revenue of each department per month. -The month has values in ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]. - -``` - - - -Reformat the table such that there is a department id column and a revenue column **for each month**. - -Return the result table in **any order**. - -The result format is in the following example. - - - -**Example 1:** - -``` - -**Input:** -Department table: -+------+---------+-------+ -| id | revenue | month | -+------+---------+-------+ -| 1 | 8000 | Jan | -| 2 | 9000 | Jan | -| 3 | 10000 | Feb | -| 1 | 7000 | Feb | -| 1 | 6000 | Mar | -+------+---------+-------+ -**Output:** -+------+-------------+-------------+-------------+-----+-------------+ -| id | Jan_Revenue | Feb_Revenue | Mar_Revenue | ... | Dec_Revenue | -+------+-------------+-------------+-------------+-----+-------------+ -| 1 | 8000 | 7000 | 6000 | ... | null | -| 2 | 9000 | null | null | ... | null | -| 3 | null | 10000 | null | ... | null | -+------+-------------+-------------+-------------+-----+-------------+ -**Explanation:** The revenue from Apr to Dec is null. -Note that the result table has 13 columns (1 for the department id + 12 for the months). - -``` - -## Starter code (mysql) -``` -# Write your MySQL query statement below -``` diff --git a/sql_drills/sql_drills/tests/stubs.py b/sql_drills/sql_drills/tests/stubs.py index fb8cc14..9ced83e 100644 --- a/sql_drills/sql_drills/tests/stubs.py +++ b/sql_drills/sql_drills/tests/stubs.py @@ -3,40 +3,19 @@ from __future__ import annotations from contextlib import contextmanager -from pathlib import Path from typing import TYPE_CHECKING from sql_drills import _dialect from sql_drills._sandbox import Executed +from sql_drills.tests import synthetic if TYPE_CHECKING: from collections.abc import Callable, Iterator import pytest -STATEMENTS = Path(__file__).parent / "statements" -MONTHS = [ - f"{m}_Revenue" - for m in [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - ] -] -GOOD_ROWS = [ - ["1", "8000", "7000", "6000", *["NULL"] * 9], - ["2", "9000", *["NULL"] * 11], - ["3", "NULL", "10000", *["NULL"] * 10], -] +MONTHS = [f"{m}_Weight" for m in synthetic.MONTHS] +GOOD_ROWS = synthetic.PIVOT_GOOD_ROWS class StubBox: @@ -72,7 +51,3 @@ def install( def done(columns: list[str], rows: list[list[str]]) -> Executed: return Executed(ok=True, error=None, code=None, rows=rows, columns=columns) - - -def statement(slug: str) -> str: - return (STATEMENTS / f"{slug}.txt").read_text() diff --git a/sql_drills/sql_drills/tests/synthetic.py b/sql_drills/sql_drills/tests/synthetic.py new file mode 100644 index 0000000..643c89b --- /dev/null +++ b/sql_drills/sql_drills/tests/synthetic.py @@ -0,0 +1,158 @@ +"""Original statements in LeetCode's layout; real problem text is never stored.""" + +from __future__ import annotations + +MONTHS = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +] + +PIVOT = """# 9001. Pivot Shipments (Easy) +Tags: Database +Languages available: mysql, postgresql + +Table: `Shipments` + +``` + ++-------------+---------+ +| Column Name | Type | ++-------------+---------+ +| id | int | +| weight | int | +| month | varchar | ++-------------+---------+ +(id, month) is the primary key of this table. +Each row is the weight a depot shipped in one month. +The month is one of ["Jan","Feb","Mar","Apr","May","Jun", +"Jul","Aug","Sep","Oct","Nov","Dec"]. + +``` + +Turn the table into one row per depot, with a weight column **for each month**. + +Rows may come back in **any order**. + +**Example 1:** + +``` + +**Input:** +Shipments table: ++------+--------+-------+ +| id | weight | month | ++------+--------+-------+ +| 1 | 400 | Jan | +| 2 | 900 | Jan | +| 3 | 150 | Feb | +| 1 | 250 | Feb | +| 1 | 70 | Mar | ++------+--------+-------+ +**Output:** ++------+------------+------------+------------+-----+------------+ +| id | Jan_Weight | Feb_Weight | Mar_Weight | ... | Dec_Weight | ++------+------------+------------+------------+-----+------------+ +| 1 | 400 | 250 | 70 | ... | null | +| 2 | 900 | null | null | ... | null | +| 3 | null | 150 | null | ... | null | ++------+------------+------------+------------+-----+------------+ +**Explanation:** Depots ship nothing from Apr to Dec. + +``` + +## Starter code (mysql) +``` +# Write your MySQL query statement below +``` +""" + +ORDERS = """# 9002. Monthly Orders (Medium) +Tags: Database +Languages available: mysql, postgresql + +Table: `Orders` + +``` + ++---------------+---------+ +| Column Name | Type | ++---------------+---------+ +| id | int | +| region | varchar | +| status | enum | +| total | int | +| placed_on | date | ++---------------+---------+ +id is the primary key of this table. +The status column is an enum of type ["shipped", "returned"]. + +``` + +For each month and region, report the number of orders and their total, plus the +number of shipped orders and their total. + +Rows may come back in **any order**. + +**Example 1:** + +``` + +**Input:** +Orders table: ++------+--------+----------+-------+------------+ +| id | region | status | total | placed_on | ++------+--------+----------+-------+------------+ +| 121 | North | shipped | 1000 | 2018-12-18 | +| 122 | North | returned | 2000 | 2018-12-19 | +| 123 | North | shipped | 2000 | 2019-01-01 | +| 124 | South | shipped | 2000 | 2019-01-07 | ++------+--------+----------+-------+------------+ +**Output:** ++---------+--------+-------------+---------------+-------------+---------------+ +| month | region | order_count | shipped_count | order_total | shipped_total | ++---------+--------+-------------+---------------+-------------+---------------+ +| 2018-12 | North | 2 | 1 | 3000 | 1000 | +| 2019-01 | North | 1 | 1 | 2000 | 2000 | +| 2019-01 | South | 1 | 1 | 2000 | 2000 | ++---------+--------+-------------+---------------+-------------+---------------+ + +``` + +## Starter code (mysql) +``` +# Write your MySQL query statement below +``` +""" + +PIVOT_GOOD_ROWS = [ + ["1", "400", "250", "70", *["NULL"] * 9], + ["2", "900", *["NULL"] * 11], + ["3", "NULL", "150", *["NULL"] * 10], +] +ORDERS_GOOD_ROWS = [ + ["2019-01", "South", "1", "1", "2000", "2000"], + ["2018-12", "North", "2", "1", "3000.00", "1000"], + ["2019-01", "North", "1", "1", "2000", "2000"], +] + +PIVOT_SQL = ( + "SELECT id, " + + ", ".join( + f"SUM(CASE WHEN month = '{m}' THEN weight END) AS {m}_Weight" for m in MONTHS + ) + + " FROM Shipments GROUP BY id" +) +ORDERS_SQL = { + "mysql": "DATE_FORMAT(placed_on, '%Y-%m')", + "postgresql": "TO_CHAR(placed_on, 'YYYY-MM')", +} diff --git a/sql_drills/sql_drills/tests/test_lc_problem.py b/sql_drills/sql_drills/tests/test_lc_problem.py index f4c76c6..19b1342 100644 --- a/sql_drills/sql_drills/tests/test_lc_problem.py +++ b/sql_drills/sql_drills/tests/test_lc_problem.py @@ -1,50 +1,43 @@ -"""Statement text to a runnable spec, on real fetched LeetCode statements.""" +"""Statement text to a runnable spec, on synthetic LeetCode-layout statements.""" from __future__ import annotations -from pathlib import Path - from sql_drills import _lc_problem as lp - -STATEMENTS = Path(__file__).parent / "statements" +from sql_drills.tests.synthetic import ORDERS, PIVOT -def _statement(slug: str) -> str: - return (STATEMENTS / f"{slug}.txt").read_text() - - -def test_reformat_department_table_example() -> None: - problem = lp.parse_problem(_statement("reformat-department-table")) - assert problem.schemas["Department"] == [ +def test_pivot_example() -> None: + problem = lp.parse_problem(PIVOT) + assert problem.schemas["Shipments"] == [ ("id", "int"), - ("revenue", "int"), + ("weight", "int"), ("month", "varchar"), ] assert not problem.ordered (example,) = problem.examples - dept = example.inputs["Department"] - assert dept.columns == ["id", "revenue", "month"] - assert dept.rows[0] == ["1", "8000", "Jan"] + dept = example.inputs["Shipments"] + assert dept.columns == ["id", "weight", "month"] + assert dept.rows[0] == ["1", "400", "Jan"] assert len(dept.rows) == 5 - assert example.expected.columns[:2] == ["id", "Jan_Revenue"] + assert example.expected.columns[:2] == ["id", "Jan_Weight"] assert "..." in example.expected.columns - assert example.expected.rows[1] == ["2", "9000", "null", "null", "...", "null"] + assert example.expected.rows[1] == ["2", "900", "null", "null", "...", "null"] -def test_monthly_transactions_example_and_ddl() -> None: - problem = lp.parse_problem(_statement("monthly-transactions-i")) +def test_orders_example_and_ddl() -> None: + problem = lp.parse_problem(ORDERS) (example,) = problem.examples assert example.expected.columns[0] == "month" assert len(example.expected.rows) == 3 my = lp.schema_sql(problem, example, "mysql") - assert "CREATE TABLE `Transactions`" in my - assert "`state` VARCHAR(255)" in my - assert "`trans_date` DATE" in my + assert "CREATE TABLE `Orders`" in my + assert "`status` VARCHAR(255)" in my + assert "`placed_on` DATE" in my assert "(121, " not in my # values are quoted literals - assert "('121', 'US', 'approved', '1000', '2018-12-18')" in my + assert "('121', 'North', 'shipped', '1000', '2018-12-18')" in my pg = lp.schema_sql(problem, example, "postgresql") - assert 'CREATE TABLE "transactions"' in pg - assert '"trans_date" DATE' in pg + assert 'CREATE TABLE "orders"' in pg + assert '"placed_on" DATE' in pg def test_schema_nulls_and_quotes() -> None: diff --git a/sql_drills/sql_drills/tests/test_lc_problem_edges.py b/sql_drills/sql_drills/tests/test_lc_problem_edges.py index 6a917f5..62f9694 100644 --- a/sql_drills/sql_drills/tests/test_lc_problem_edges.py +++ b/sql_drills/sql_drills/tests/test_lc_problem_edges.py @@ -143,7 +143,7 @@ Output: 3 def test_order_detection_ignores_schema_blocks_and_any_order() -> None: - base = "Return the result table in **any order**.\nExample 1:\n" + base = "Rows may come back in **any order**.\nExample 1:\n" sorted_by = "Return the result sorted by id.\nExample 1:\n" in_block = "```\nthe table is ordered by id\n```\n" + base assert not lp.is_ordered(base) diff --git a/sql_drills/sql_drills/tests/test_lc_run.py b/sql_drills/sql_drills/tests/test_lc_run.py index 7610a66..0204429 100644 --- a/sql_drills/sql_drills/tests/test_lc_run.py +++ b/sql_drills/sql_drills/tests/test_lc_run.py @@ -6,7 +6,8 @@ from typing import TYPE_CHECKING import sql_drills as sd from sql_drills._sandbox import Executed -from sql_drills.tests.stubs import GOOD_ROWS, MONTHS, done, install, statement +from sql_drills.tests.stubs import GOOD_ROWS, MONTHS, done, install +from sql_drills.tests.synthetic import ORDERS, PIVOT if TYPE_CHECKING: import pytest @@ -16,13 +17,13 @@ def test_elided_columns_compare_only_the_named_ones( monkeypatch: pytest.MonkeyPatch, ) -> None: schemas, boxes = install(monkeypatch, done(["id", *MONTHS], GOOD_ROWS)) - (res,) = sd.run_problem(statement("reformat-department-table"), "SELECT 1", "mysql") + (res,) = sd.run_problem(PIVOT, "SELECT 1", "mysql") assert res.ok assert res.match, res.cell_diff assert res.note is not None assert "..." in res.note assert schemas[0] == "dialect=mysql" - assert schemas[1].startswith("CREATE TABLE `Department`") + assert schemas[1].startswith("CREATE TABLE `Shipments`") assert boxes[0].sqls == ["SELECT 1"] assert res.columns == ["id", *MONTHS] @@ -32,9 +33,7 @@ def test_postgres_lowercase_names_and_row_order_are_fine( ) -> None: cols = ["id", *[m.lower() for m in MONTHS]] install(monkeypatch, done(cols, list(reversed(GOOD_ROWS)))) - (res,) = sd.run_problem( - statement("reformat-department-table"), "SELECT 1", "postgresql" - ) + (res,) = sd.run_problem(PIVOT, "SELECT 1", "postgresql") assert res.match @@ -42,7 +41,7 @@ def test_wrong_value_gives_a_cell_diff(monkeypatch: pytest.MonkeyPatch) -> None: bad = [list(r) for r in GOOD_ROWS] bad[0][1] = "1" install(monkeypatch, done(["id", *MONTHS], bad)) - (res,) = sd.run_problem(statement("reformat-department-table"), "x", "mysql") + (res,) = sd.run_problem(PIVOT, "x", "mysql") assert not res.match assert res.cell_diff assert res.missing_rows @@ -52,7 +51,7 @@ def test_wrong_value_gives_a_cell_diff(monkeypatch: pytest.MonkeyPatch) -> None: def test_missing_named_column_does_not_match(monkeypatch: pytest.MonkeyPatch) -> None: rows = [r[:-1] for r in GOOD_ROWS] install(monkeypatch, done(["id", *MONTHS[:-1]], rows)) - (res,) = sd.run_problem(statement("reformat-department-table"), "x", "mysql") + (res,) = sd.run_problem(PIVOT, "x", "mysql") assert not res.match @@ -61,7 +60,7 @@ def test_query_error_is_reported_per_example(monkeypatch: pytest.MonkeyPatch) -> ok=False, error='ERROR: column "Jan" does not exist', code=None, rows=[] ) install(monkeypatch, err) - (res,) = sd.run_problem(statement("reformat-department-table"), "x", "postgresql") + (res,) = sd.run_problem(PIVOT, "x", "postgresql") assert not res.ok assert not res.match assert res.error == 'ERROR: column "Jan" does not exist' @@ -69,27 +68,25 @@ def test_query_error_is_reported_per_example(monkeypatch: pytest.MonkeyPatch) -> assert res.expected_rows[0][0] == "1" -def test_monthly_transactions_unordered_match(monkeypatch: pytest.MonkeyPatch) -> None: +def test_orders_unordered_match(monkeypatch: pytest.MonkeyPatch) -> None: rows = [ - ["2019-01", "DE", "1", "1", "2000", "2000"], - ["2018-12", "US", "2", "1", "3000.00", "1000"], - ["2019-01", "US", "1", "1", "2000", "2000"], + ["2019-01", "South", "1", "1", "2000", "2000"], + ["2018-12", "North", "2", "1", "3000.00", "1000"], + ["2019-01", "North", "1", "1", "2000", "2000"], ] - cols = ["month", "country", "trans_count", "approved_count", "a", "b"] + cols = ["month", "region", "order_count", "shipped_count", "a", "b"] install(monkeypatch, done(cols, rows)) - (res,) = sd.run_problem(statement("monthly-transactions-i"), "x", "mysql") + (res,) = sd.run_problem(ORDERS, "x", "mysql") assert res.match assert res.note is None def test_ordered_statement_demands_order(monkeypatch: pytest.MonkeyPatch) -> None: - text = statement("monthly-transactions-i").replace( - "**any order**", "ordered by month" - ) + text = ORDERS.replace("**any order**", "ordered by month") rows = [ - ["2019-01", "DE", "1", "1", "2000", "2000"], - ["2018-12", "US", "2", "1", "3000", "1000"], - ["2019-01", "US", "1", "1", "2000", "2000"], + ["2019-01", "South", "1", "1", "2000", "2000"], + ["2018-12", "North", "2", "1", "3000", "1000"], + ["2019-01", "North", "1", "1", "2000", "2000"], ] install(monkeypatch, done(["a"] * 6, rows)) (res,) = sd.run_problem(text, "x", "mysql") diff --git a/sql_drills/sql_drills/tests/test_lc_run_inputs.py b/sql_drills/sql_drills/tests/test_lc_run_inputs.py index 133bfba..0fd637e 100644 --- a/sql_drills/sql_drills/tests/test_lc_run_inputs.py +++ b/sql_drills/sql_drills/tests/test_lc_run_inputs.py @@ -7,7 +7,8 @@ from typing import TYPE_CHECKING, cast import pytest import sql_drills as sd -from sql_drills.tests.stubs import GOOD_ROWS, MONTHS, done, install, statement +from sql_drills.tests.stubs import GOOD_ROWS, MONTHS, done, install +from sql_drills.tests.synthetic import PIVOT if TYPE_CHECKING: from sql_drills._dialect import Dialect @@ -64,8 +65,8 @@ def test_statement_without_examples_raises(monkeypatch: pytest.MonkeyPatch) -> N @pytest.mark.parametrize( "arg", [ - "reformat-department-table", - "https://leetcode.com/problems/reformat-department-table/description/", + "pivot-shipments", + "https://leetcode.com/problems/pivot-shipments/description/", ], ) def test_slug_and_url_are_fetched(monkeypatch: pytest.MonkeyPatch, arg: str) -> None: @@ -73,9 +74,9 @@ def test_slug_and_url_are_fetched(monkeypatch: pytest.MonkeyPatch, arg: str) -> def fetch(slug: str, dialect: str) -> str: asked.append((slug, dialect)) - return statement("reformat-department-table") + return PIVOT install(monkeypatch, done(["id", *MONTHS], GOOD_ROWS)) (res,) = sd.run_problem(arg, "x", "postgresql", fetch=fetch) assert res.match - assert asked == [("reformat-department-table", "postgresql")] + assert asked == [("pivot-shipments", "postgresql")] diff --git a/sql_drills/sql_drills/tests/test_live_leetcode.py b/sql_drills/sql_drills/tests/test_live_leetcode.py new file mode 100644 index 0000000..054ebd5 --- /dev/null +++ b/sql_drills/sql_drills/tests/test_live_leetcode.py @@ -0,0 +1,91 @@ +"""Real LeetCode problems, fetched at runtime and never stored in the repo. + +Needs the network and the live containers; skipped when either is missing. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +import sql_drills as sd +from sql_drills import _engine, _lc_run, _pg_engine + +if TYPE_CHECKING: + from collections.abc import Callable + + from sql_drills._dialect import Dialect + +MONTHS = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +] +PIVOT_SQL = ( + "SELECT id, " + + ", ".join( + f"SUM(CASE WHEN month = '{m}' THEN revenue END) AS {m}_Revenue" for m in MONTHS + ) + + " FROM Department GROUP BY id" +) +MONTHLY_SQL = { + "mysql": "DATE_FORMAT(trans_date, '%Y-%m')", + "postgresql": "TO_CHAR(trans_date, 'YYYY-MM')", +} + + +def _ready(check: Callable[[], bool]) -> bool: + try: + return check() + except _engine.EngineError: + return False + + +pytestmark = [ + pytest.mark.docker, + pytest.mark.network, + pytest.mark.skipif( + not _ready(_pg_engine.is_ready), reason="sql-drills-postgres is not running" + ), +] + + +def _fetch(slug: str, dialect: str) -> str: + try: + return _lc_run.default_fetch(slug, dialect) + except sd.ProblemUnavailableError as exc: + return pytest.skip(str(exc)) + + +def test_reformat_department_table() -> None: + text = _fetch("reformat-department-table", "postgresql") + (res,) = sd.run_problem(text, PIVOT_SQL, "postgresql") + assert res.match, (res.error, res.cell_diff) + quoted = PIVOT_SQL.replace("month = 'Jan'", 'month = "Jan"') + (bad,) = sd.run_problem(text, quoted, "postgresql") + assert 'column "Jan" does not exist' in (bad.error or "") + + +@pytest.mark.parametrize("dialect", ["postgresql", "mysql"]) +def test_monthly_transactions_i(dialect: Dialect) -> None: + if dialect == "mysql" and not _ready(_engine.is_ready): + pytest.skip("mysql container is down") + month = MONTHLY_SQL[dialect] + sql = ( + f"SELECT {month}, country, COUNT(*), " + "SUM(CASE WHEN state = 'approved' THEN 1 ELSE 0 END), SUM(amount), " + "SUM(CASE WHEN state = 'approved' THEN amount ELSE 0 END) " + f"FROM Transactions GROUP BY {month}, country" + ) + (res,) = sd.run_problem(_fetch("monthly-transactions-i", dialect), sql, dialect) + assert res.match, (res.error, res.cell_diff) diff --git a/sql_drills/sql_drills/tests/test_live_pg.py b/sql_drills/sql_drills/tests/test_live_pg.py index a0611d4..7b82f2f 100644 --- a/sql_drills/sql_drills/tests/test_live_pg.py +++ b/sql_drills/sql_drills/tests/test_live_pg.py @@ -6,44 +6,19 @@ touch the MySQL container's data, and only use their own ``drill_*`` databases. from __future__ import annotations -from pathlib import Path from typing import TYPE_CHECKING import pytest import sql_drills as sd from sql_drills import _engine, _pg_engine, _pg_sandbox +from sql_drills.tests.synthetic import ORDERS, ORDERS_SQL, PIVOT, PIVOT_SQL if TYPE_CHECKING: from collections.abc import Callable from sql_drills._dialect import Dialect -STATEMENTS = Path(__file__).parent / "statements" -MONTHS = [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", -] -_PIVOT_COLS = ", ".join( - f"SUM(CASE WHEN month = '{m}' THEN revenue END) AS {m}_Revenue" for m in MONTHS -) -PIVOT = f"SELECT id, {_PIVOT_COLS} FROM Department GROUP BY id" -PIVOT_JAN_QUOTED = PIVOT.replace("month = 'Jan'", 'month = "Jan"') -MONTHLY = { - "mysql": "DATE_FORMAT(trans_date, '%Y-%m')", - "postgresql": "TO_CHAR(trans_date, 'YYYY-MM')", -} - def _up(check: Callable[[], bool]) -> bool: try: @@ -60,56 +35,48 @@ pytestmark = [ ] -def _text(slug: str) -> str: - return (STATEMENTS / f"{slug}.txt").read_text() - - -def _monthly(dialect: Dialect) -> str: - return ( - f"SELECT {MONTHLY[dialect]} AS month, country, COUNT(*), " - "SUM(CASE WHEN state = 'approved' THEN 1 ELSE 0 END), SUM(amount), " - "SUM(CASE WHEN state = 'approved' THEN amount ELSE 0 END) " - f"FROM Transactions GROUP BY {MONTHLY[dialect]}, country" - ) - - def _roles_and_databases() -> tuple[set[str], set[str]]: dbs = _pg_engine.run_sql("SELECT datname FROM pg_database", header=False) roles = _pg_engine.run_sql("SELECT rolname FROM pg_roles", header=False) return set(dbs.stdout.split()), set(roles.stdout.split()) -def test_reformat_department_table_passes_in_postgres() -> None: - (res,) = sd.run_problem(_text("reformat-department-table"), PIVOT, "postgresql") +def test_pivot_passes_in_postgres() -> None: + (res,) = sd.run_problem(PIVOT, PIVOT_SQL, "postgresql") assert res.ok, res.error assert res.match, res.cell_diff assert len(res.columns) == 13 def test_double_quoted_month_fails_in_postgres_but_passes_in_mysql() -> None: - statement = _text("reformat-department-table") - (pg,) = sd.run_problem(statement, PIVOT_JAN_QUOTED, "postgresql") + quoted = PIVOT_SQL.replace("month = 'Jan'", 'month = "Jan"') + (pg,) = sd.run_problem(PIVOT, quoted, "postgresql") assert not pg.ok assert 'column "Jan" does not exist' in (pg.error or "") if not _up(_engine.is_ready): pytest.skip("mysql container is down") - (my,) = sd.run_problem(statement, PIVOT_JAN_QUOTED, "mysql") + (my,) = sd.run_problem(PIVOT, quoted, "mysql") assert my.match @pytest.mark.parametrize("dialect", ["postgresql", "mysql"]) -def test_monthly_transactions_passes_in_both_dialects(dialect: Dialect) -> None: +def test_orders_pass_in_both_dialects(dialect: Dialect) -> None: if dialect == "mysql" and not _up(_engine.is_ready): pytest.skip("mysql container is down") - (res,) = sd.run_problem(_text("monthly-transactions-i"), _monthly(dialect), dialect) + month = ORDERS_SQL[dialect] + sql = ( + f"SELECT {month}, region, COUNT(*), " + "SUM(CASE WHEN status = 'shipped' THEN 1 ELSE 0 END), SUM(total), " + "SUM(CASE WHEN status = 'shipped' THEN total ELSE 0 END) " + f"FROM Orders GROUP BY {month}, region" + ) + (res,) = sd.run_problem(ORDERS, sql, dialect) assert res.ok, res.error assert res.match, (res.cell_diff, res.rows) def test_wrong_answer_reports_cells() -> None: - (res,) = sd.run_problem( - _text("monthly-transactions-i"), "SELECT 1, 2, 3, 4, 5, 6", "postgresql" - ) + (res,) = sd.run_problem(ORDERS, "SELECT 1, 2, 3, 4, 5, 6", "postgresql") assert res.ok assert not res.match assert res.cell_diff @@ -117,9 +84,7 @@ def test_wrong_answer_reports_cells() -> None: def test_runs_are_isolated_and_leave_nothing_behind() -> None: before = _roles_and_databases() - sd.run_problem( - _text("monthly-transactions-i"), "DELETE FROM Transactions", "postgresql" - ) + sd.run_problem(ORDERS, "DELETE FROM Orders", "postgresql") assert _roles_and_databases() == before