Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.10', '3.11']
python-version: ['3.10', '3.11', '3.12']
library: ['client', 'server', 'djqs', 'djrs']

steps:
Expand Down
2 changes: 1 addition & 1 deletion datajunction-query/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies = [
"importlib-metadata",
"accept-types==0.4.1",
"cachelib>=0.4.0",
"duckdb==0.8.1",
"duckdb>=1.0.0",
"duckdb-engine",
"fastapi>=0.79.0",
"msgpack>=1.0.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ async def get_context(
# session_context()) can reuse it in tests. resolver_session() does NOT
# use this — it checks dependency_overrides instead, so it always
# creates independent sessions in production.
if not hasattr(request.state, "test_session"):
if not hasattr(request.state, "test_session"): # pragma: no branch
request.state.test_session = db_session

return {
Expand Down
16 changes: 8 additions & 8 deletions datajunction-server/datajunction_server/api/graphql/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,14 @@ async def resolver_session(info: Info) -> AsyncIterator[AsyncSession]:
if override is not None:
yield override()
return

# Production: create a genuinely independent session per resolver.
gen = get_session(request, session_label="graphql_resolver")
session = await gen.__anext__()
try:
yield session
finally:
await gen.aclose() # type: ignore
else: # pragma: no cover
# Production: create a genuinely independent session per resolver.
gen = get_session(request, session_label="graphql_resolver")
session = await gen.__anext__()
try:
yield session
finally:
await gen.aclose() # type: ignore


def convert_camel_case(name):
Expand Down
2 changes: 1 addition & 1 deletion datajunction-server/datajunction_server/internal/impact.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ async def _revalidate_and_apply(

async def _parse_all_queries() -> dict[str, ast.Query | Exception]:
"""Parse all queries in a threadpool."""
loop = asyncio.get_event_loop()
loop = asyncio.get_running_loop()
parsed: dict[str, ast.Query | Exception] = {}
with ThreadPoolExecutor(max_workers=min(8, len(queries_to_parse) or 1)) as pool:
futures = {
Expand Down
5 changes: 3 additions & 2 deletions datajunction-server/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,13 @@ dependencies = [
# JWT for GitHub App authentication
"pyjwt[crypto]>=2.8.0",
]
requires-python = ">=3.10,<3.12"
requires-python = ">=3.10,<3.13"
readme = "README.md"
license = {text = "MIT"}
classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent"
]
Expand Down Expand Up @@ -160,7 +161,7 @@ test = [
"requests-mock>=1.10.0",
"typing-extensions>=4.5.0",
"pytest-xdist>=3.3.0",
"duckdb==0.8.1",
"duckdb>=1.0.0",
"testcontainers>=3.7.1",
"httpx>=0.27.0",
"greenlet>=3.0.3",
Expand Down
151 changes: 90 additions & 61 deletions datajunction-server/tests/api/cubes_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2649,20 +2649,24 @@ async def test_get_unmaterialized_cube_dimensions_values(
},
)
results = response.json()
results["values"].sort(key=lambda v: str(v["value"]))
assert results == {
"cardinality": 9,
"dimensions": ["default.hard_hat.city"],
"values": [
{"count": None, "value": ["Jersey City"]},
{"count": None, "value": ["Billerica"]},
{"count": None, "value": ["Southgate"]},
{"count": None, "value": ["Phoenix"]},
{"count": None, "value": ["Southampton"]},
{"count": None, "value": ["Powder Springs"]},
{"count": None, "value": ["Middletown"]},
{"count": None, "value": ["Muskogee"]},
{"count": None, "value": ["Niagara Falls"]},
],
"values": sorted(
[
{"count": None, "value": ["Jersey City"]},
{"count": None, "value": ["Billerica"]},
{"count": None, "value": ["Southgate"]},
{"count": None, "value": ["Phoenix"]},
{"count": None, "value": ["Southampton"]},
{"count": None, "value": ["Powder Springs"]},
{"count": None, "value": ["Middletown"]},
{"count": None, "value": ["Muskogee"]},
{"count": None, "value": ["Niagara Falls"]},
],
key=lambda v: str(v["value"]),
),
}

# Ask for single dimension with counts
Expand All @@ -2686,20 +2690,25 @@ async def test_get_unmaterialized_cube_dimensions_values(
"include_counts": True,
},
)
assert response.json() == {
results = response.json()
results["values"].sort(key=lambda v: str(v["value"]))
assert results == {
"cardinality": 9,
"dimensions": ["default.hard_hat.city"],
"values": [
{"count": 5, "value": ["Southgate"]},
{"count": 4, "value": ["Jersey City"]},
{"count": 4, "value": ["Southampton"]},
{"count": 3, "value": ["Billerica"]},
{"count": 3, "value": ["Powder Springs"]},
{"count": 2, "value": ["Phoenix"]},
{"count": 2, "value": ["Middletown"]},
{"count": 1, "value": ["Muskogee"]},
{"count": 1, "value": ["Niagara Falls"]},
],
"values": sorted(
[
{"count": 5, "value": ["Southgate"]},
{"count": 4, "value": ["Jersey City"]},
{"count": 4, "value": ["Southampton"]},
{"count": 3, "value": ["Billerica"]},
{"count": 3, "value": ["Powder Springs"]},
{"count": 2, "value": ["Phoenix"]},
{"count": 2, "value": ["Middletown"]},
{"count": 1, "value": ["Muskogee"]},
{"count": 1, "value": ["Niagara Falls"]},
],
key=lambda v: str(v["value"]),
),
}

# Get data for multiple dimensions with counts
Expand All @@ -2710,28 +2719,33 @@ async def test_get_unmaterialized_cube_dimensions_values(
"include_counts": True,
},
)
assert response.json() == {
results = response.json()
results["values"].sort(key=lambda v: str(v["value"]))
assert results == {
"cardinality": 17,
"dimensions": ["default.hard_hat.city", "default.dispatcher.company_name"],
"values": [
{"count": 3, "value": ["Jersey City", "Pothole Pete"]},
{"count": 2, "value": ["Southgate", "Asphalts R Us"]},
{"count": 2, "value": ["Billerica", "Asphalts R Us"]},
{"count": 2, "value": ["Southgate", "Federal Roads Group"]},
{"count": 2, "value": ["Southampton", "Pothole Pete"]},
{"count": 2, "value": ["Powder Springs", "Asphalts R Us"]},
{"count": 2, "value": ["Middletown", "Federal Roads Group"]},
{"count": 1, "value": ["Jersey City", "Federal Roads Group"]},
{"count": 1, "value": ["Billerica", "Pothole Pete"]},
{"count": 1, "value": ["Phoenix", "Asphalts R Us"]},
{"count": 1, "value": ["Southampton", "Asphalts R Us"]},
{"count": 1, "value": ["Southampton", "Federal Roads Group"]},
{"count": 1, "value": ["Phoenix", "Federal Roads Group"]},
{"count": 1, "value": ["Muskogee", "Federal Roads Group"]},
{"count": 1, "value": ["Powder Springs", "Pothole Pete"]},
{"count": 1, "value": ["Niagara Falls", "Federal Roads Group"]},
{"count": 1, "value": ["Southgate", "Pothole Pete"]},
],
"values": sorted(
[
{"count": 3, "value": ["Jersey City", "Pothole Pete"]},
{"count": 2, "value": ["Southgate", "Asphalts R Us"]},
{"count": 2, "value": ["Billerica", "Asphalts R Us"]},
{"count": 2, "value": ["Southgate", "Federal Roads Group"]},
{"count": 2, "value": ["Southampton", "Pothole Pete"]},
{"count": 2, "value": ["Powder Springs", "Asphalts R Us"]},
{"count": 2, "value": ["Middletown", "Federal Roads Group"]},
{"count": 1, "value": ["Jersey City", "Federal Roads Group"]},
{"count": 1, "value": ["Billerica", "Pothole Pete"]},
{"count": 1, "value": ["Phoenix", "Asphalts R Us"]},
{"count": 1, "value": ["Southampton", "Asphalts R Us"]},
{"count": 1, "value": ["Southampton", "Federal Roads Group"]},
{"count": 1, "value": ["Phoenix", "Federal Roads Group"]},
{"count": 1, "value": ["Muskogee", "Federal Roads Group"]},
{"count": 1, "value": ["Powder Springs", "Pothole Pete"]},
{"count": 1, "value": ["Niagara Falls", "Federal Roads Group"]},
{"count": 1, "value": ["Southgate", "Pothole Pete"]},
],
key=lambda v: str(v["value"]),
),
}

# Get data for multiple dimensions with filters
Expand All @@ -2743,16 +2757,21 @@ async def test_get_unmaterialized_cube_dimensions_values(
"include_counts": True,
},
)
assert response.json() == {
results = response.json()
results["values"].sort(key=lambda v: str(v["value"]))
assert results == {
"cardinality": 5,
"dimensions": ["default.hard_hat.city", "default.dispatcher.company_name"],
"values": [
{"count": 3, "value": ["Jersey City", "Pothole Pete"]},
{"count": 2, "value": ["Southampton", "Pothole Pete"]},
{"count": 1, "value": ["Billerica", "Pothole Pete"]},
{"count": 1, "value": ["Southgate", "Pothole Pete"]},
{"count": 1, "value": ["Powder Springs", "Pothole Pete"]},
],
"values": sorted(
[
{"count": 3, "value": ["Jersey City", "Pothole Pete"]},
{"count": 2, "value": ["Southampton", "Pothole Pete"]},
{"count": 1, "value": ["Billerica", "Pothole Pete"]},
{"count": 1, "value": ["Southgate", "Pothole Pete"]},
{"count": 1, "value": ["Powder Springs", "Pothole Pete"]},
],
key=lambda v: str(v["value"]),
),
}

# Get data for multiple dimensions with filters and limit
Expand All @@ -2765,16 +2784,26 @@ async def test_get_unmaterialized_cube_dimensions_values(
"include_counts": True,
},
)
assert response.json() == {
"cardinality": 4,
"dimensions": ["default.hard_hat.city", "default.dispatcher.company_name"],
"values": [
{"count": 3, "value": ["Jersey City", "Pothole Pete"]},
{"count": 2, "value": ["Southampton", "Pothole Pete"]},
{"count": 1, "value": ["Billerica", "Pothole Pete"]},
{"count": 1, "value": ["Southgate", "Pothole Pete"]},
],
}
results = response.json()
assert results["cardinality"] == 4
assert results["dimensions"] == [
"default.hard_hat.city",
"default.dispatcher.company_name",
]
values = results["values"]
# Top 2 are deterministic by count; last 2 are a 2-subset of the 3
# count=1 Pothole Pete rows (tie-break is duckdb-version dependent).
assert values[:2] == [
{"count": 3, "value": ["Jersey City", "Pothole Pete"]},
{"count": 2, "value": ["Southampton", "Pothole Pete"]},
]
possible_tail = [
{"count": 1, "value": ["Billerica", "Pothole Pete"]},
{"count": 1, "value": ["Powder Springs", "Pothole Pete"]},
{"count": 1, "value": ["Southgate", "Pothole Pete"]},
]
assert len(values) == 4
assert all(v in possible_tail for v in values[2:])


@pytest.mark.asyncio
Expand Down
14 changes: 8 additions & 6 deletions datajunction-server/tests/api/sql_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3244,12 +3244,14 @@ async def test_get_sql_including_dimensions_with_disambiguated_columns(
),
)
result = duckdb_conn.sql(data["sql"])
assert result.fetchall() == [
(33, "A", None, "New York", 285627.0),
(44, "A", None, "Dallas", 18497.0),
(44, "A", None, "San Antonio", 76463.0),
(39, "B", None, "Philadelphia", 1135603.0),
]
assert sorted(result.fetchall()) == sorted(
[
(33, "A", None, "New York", 285627.0),
(44, "A", None, "Dallas", 18497.0),
(44, "A", None, "San Antonio", 76463.0),
(39, "B", None, "Philadelphia", 1135603.0),
],
)

response = await client_with_roads.get(
"/sql/",
Expand Down
63 changes: 35 additions & 28 deletions datajunction-server/tests/api/sql_v2_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1760,12 +1760,14 @@ async def test_metric_definitions_with_single_joinable_dimensions(
"""
assert str(parse(data[0]["sql"])) == str(parse(expected_sql))
result = duckdb_conn.sql(data[0]["sql"])
assert result.fetchall() == [
("Alexander Wilkinson", "Alexander Wilkinson"),
("Virgil Craft", "Virgil Craft"),
("Chester Lyon", "Chester Lyon"),
("Willie Chaney", "Willie Chaney"),
]
assert sorted(result.fetchall()) == sorted(
[
("Alexander Wilkinson", "Alexander Wilkinson"),
("Virgil Craft", "Virgil Craft"),
("Chester Lyon", "Chester Lyon"),
("Willie Chaney", "Willie Chaney"),
],
)

@pytest.mark.asyncio
async def test_metric_definition_with_multiple_joinable_dimensions(
Expand Down Expand Up @@ -1879,17 +1881,20 @@ async def test_metric_definition_with_multiple_joinable_dimensions(
"""
assert str(parse(data[0]["sql"])) == str(parse(expected_sql))
result = duckdb_conn.sql(data[0]["sql"])
assert result.fetchall() == [
("Jersey City", "Perkins", "NJ", None),
("Billerica", "Best", "MA", None),
("Southgate", "Riley", "MI", None),
("Phoenix", "Henderson", "AZ", None),
("Southampton", "Stafford", "PA", None),
("Powder Springs", "Clarke", "GA", None),
("Middletown", "Massey", "CT", None),
("Muskogee", "Ziegler", "OK", None),
("Niagara Falls", "Boone", "NY", "Boone"),
]
assert sorted(result.fetchall(), key=lambda r: r[0]) == sorted(
[
("Jersey City", "Perkins", "NJ", None),
("Billerica", "Best", "MA", None),
("Southgate", "Riley", "MI", None),
("Phoenix", "Henderson", "AZ", None),
("Southampton", "Stafford", "PA", None),
("Powder Springs", "Clarke", "GA", None),
("Middletown", "Massey", "CT", None),
("Muskogee", "Ziegler", "OK", None),
("Niagara Falls", "Boone", "NY", "Boone"),
],
key=lambda r: r[0],
)

@pytest.mark.asyncio
async def test_sql_metric_definition_with_multiple_joinable_dimensions(
Expand Down Expand Up @@ -1975,17 +1980,19 @@ async def test_sql_metric_definition_with_multiple_joinable_dimensions(
"""
assert str(parse(data["sql"])) == str(parse(expected_sql))
result = duckdb_conn.sql(data["sql"])
assert result.fetchall() == [
("Perkins", "NJ", 0),
("Best", "MA", 0),
("Riley", "MI", 0),
("Henderson", "AZ", 0),
("Stafford", "PA", 0),
("Clarke", "GA", 0),
("Massey", "CT", 0),
("Ziegler", "OK", 0),
("Boone", "NY", 1),
]
assert sorted(result.fetchall()) == sorted(
[
("Perkins", "NJ", 0),
("Best", "MA", 0),
("Riley", "MI", 0),
("Henderson", "AZ", 0),
("Stafford", "PA", 0),
("Clarke", "GA", 0),
("Massey", "CT", 0),
("Ziegler", "OK", 0),
("Boone", "NY", 1),
],
)


@pytest.mark.asyncio
Expand Down
Loading
Loading