mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-15 05:50:58 +01:00
Replace LeetCode fixtures with synthetic ones
Public repo: statements are original, in LeetCode's layout. Real problems are fetched at runtime by network-marked tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWYGW5NZL5uVpnqmNKo
This commit is contained in:
parent
716231a7f3
commit
737bf624ca
@ -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*"]
|
||||
|
||||
@ -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
|
||||
```
|
||||
@ -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
|
||||
```
|
||||
@ -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()
|
||||
|
||||
158
sql_drills/sql_drills/tests/synthetic.py
Normal file
158
sql_drills/sql_drills/tests/synthetic.py
Normal file
@ -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')",
|
||||
}
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")]
|
||||
|
||||
91
sql_drills/sql_drills/tests/test_live_leetcode.py
Normal file
91
sql_drills/sql_drills/tests/test_live_leetcode.py
Normal file
@ -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)
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user