This repository was archived by the owner on May 7, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 199
feat: add DuckDB data source with v3 metadata API support #1436
Merged
Merged
Changes from 4 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
0cb32e1
feat: add duckdb data source with v3 metadata API support
goldmedal 79a0b45
fix: resolve CodeQL path-injection alerts in resolve_connection_info
goldmedal 6a4617e
Potential fix for code scanning alert no. 47: Uncontrolled data used …
goldmedal 6a105ef
Potential fix for code scanning alert no. 48: Uncontrolled data used …
goldmedal b6d58b9
fix: use os.path.normpath(join) pattern for CodeQL path-injection com…
goldmedal 7a9b950
fix: avoid double separator in root prefix when allowed root is files…
goldmedal 58b5ee4
chore: fix fmt
goldmedal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import pathlib | ||
|
|
||
| import pytest | ||
|
|
||
| pytestmark = pytest.mark.duckdb | ||
|
|
||
| base_url = "/v3/connector/duckdb" | ||
|
|
||
|
|
||
| def pytest_collection_modifyitems(items): | ||
| current_file_dir = pathlib.Path(__file__).resolve().parent | ||
| for item in items: | ||
| try: | ||
| pathlib.Path(item.fspath).relative_to(current_file_dir) | ||
| item.add_marker(pytestmark) | ||
| except ValueError: | ||
| pass | ||
|
|
||
|
|
||
| @pytest.fixture(scope="module") | ||
| def connection_info() -> dict[str, str]: | ||
| return { | ||
| "url": "tests/resource/duckdb", | ||
| "format": "duckdb", | ||
| } |
44 changes: 44 additions & 0 deletions
44
ibis-server/tests/routers/v3/connector/duckdb/test_metadata.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| from tests.routers.v3.connector.duckdb.conftest import base_url | ||
|
|
||
| v3_base_url = base_url | ||
|
|
||
|
|
||
| async def test_metadata_list_tables(client, connection_info): | ||
| response = await client.post( | ||
| url=f"{v3_base_url}/metadata/tables", | ||
| json={"connectionInfo": connection_info}, | ||
| ) | ||
| assert response.status_code == 200 | ||
|
|
||
| tables = response.json() | ||
| assert len(tables) > 0 | ||
|
|
||
| result = next( | ||
| filter(lambda x: x["name"] == "main.customers", tables), | ||
| None, | ||
| ) | ||
| assert result is not None | ||
| assert result["primaryKey"] == "" | ||
| assert result["properties"] == { | ||
| "catalog": "jaffle_shop", | ||
| "schema": "main", | ||
| "table": "customers", | ||
| "path": None, | ||
| } | ||
| assert len(result["columns"]) > 0 | ||
|
|
||
| customer_id_col = next( | ||
| filter(lambda c: c["name"] == "customer_id", result["columns"]), None | ||
| ) | ||
| assert customer_id_col is not None | ||
| assert customer_id_col["nestedColumns"] is None | ||
| assert customer_id_col["properties"] is None | ||
|
|
||
|
|
||
| async def test_metadata_list_constraints(client, connection_info): | ||
| response = await client.post( | ||
| url=f"{v3_base_url}/metadata/constraints", | ||
| json={"connectionInfo": connection_info}, | ||
| ) | ||
| assert response.status_code == 200 | ||
| assert response.json() == [] |
137 changes: 137 additions & 0 deletions
137
ibis-server/tests/routers/v3/connector/duckdb/test_query.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| import base64 | ||
|
|
||
| import orjson | ||
| import pytest | ||
|
|
||
| from tests.routers.v3.connector.duckdb.conftest import base_url | ||
|
|
||
| manifest = { | ||
| "catalog": "wren", | ||
| "schema": "public", | ||
| "models": [ | ||
| { | ||
| "name": "customers", | ||
| "tableReference": { | ||
| "catalog": "jaffle_shop", | ||
| "schema": "main", | ||
| "table": "customers", | ||
| }, | ||
| "columns": [ | ||
| {"name": "customer_id", "type": "integer"}, | ||
| {"name": "first_name", "type": "varchar"}, | ||
| {"name": "last_name", "type": "varchar"}, | ||
| {"name": "first_order", "type": "date"}, | ||
| {"name": "most_recent_order", "type": "date"}, | ||
| {"name": "number_of_orders", "type": "bigint"}, | ||
| {"name": "customer_lifetime_value", "type": "double"}, | ||
| ], | ||
| "primaryKey": "customer_id", | ||
| }, | ||
| { | ||
| "name": "orders", | ||
| "tableReference": { | ||
| "catalog": "jaffle_shop", | ||
| "schema": "main", | ||
| "table": "orders", | ||
| }, | ||
| "columns": [ | ||
| {"name": "order_id", "type": "integer"}, | ||
| {"name": "customer_id", "type": "integer"}, | ||
| {"name": "order_date", "type": "date"}, | ||
| {"name": "status", "type": "varchar"}, | ||
| {"name": "amount", "type": "double"}, | ||
| ], | ||
| "primaryKey": "order_id", | ||
| }, | ||
| ], | ||
| "relationships": [ | ||
| { | ||
| "name": "CustomersOrders", | ||
| "models": ["customers", "orders"], | ||
| "joinType": "ONE_TO_MANY", | ||
| "condition": '"customers".customer_id = "orders".customer_id', | ||
| } | ||
| ], | ||
| } | ||
|
|
||
|
|
||
| @pytest.fixture(scope="module") | ||
| def manifest_str(): | ||
| return base64.b64encode(orjson.dumps(manifest)).decode("utf-8") | ||
|
|
||
|
|
||
| async def test_query(client, manifest_str, connection_info): | ||
| response = await client.post( | ||
| f"{base_url}/query", | ||
| json={ | ||
| "manifestStr": manifest_str, | ||
| "sql": 'SELECT customer_id, first_name, last_name FROM "customers" ORDER BY customer_id LIMIT 1', | ||
| "connectionInfo": connection_info, | ||
| }, | ||
| ) | ||
| assert response.status_code == 200 | ||
| result = response.json() | ||
| assert result["columns"] == ["customer_id", "first_name", "last_name"] | ||
| assert len(result["data"]) == 1 | ||
| assert result["data"][0] == [1, "Michael", "P."] | ||
| assert result["dtypes"] == { | ||
| "customer_id": "int32", | ||
| "first_name": "string", | ||
| "last_name": "string", | ||
| } | ||
|
|
||
|
|
||
| async def test_query_with_limit(client, manifest_str, connection_info): | ||
| response = await client.post( | ||
| f"{base_url}/query", | ||
| params={"limit": 1}, | ||
| json={ | ||
| "manifestStr": manifest_str, | ||
| "sql": 'SELECT * FROM "customers" LIMIT 5', | ||
| "connectionInfo": connection_info, | ||
| }, | ||
| ) | ||
| assert response.status_code == 200 | ||
| result = response.json() | ||
| assert len(result["data"]) == 1 | ||
|
|
||
|
|
||
| async def test_query_orders(client, manifest_str, connection_info): | ||
| response = await client.post( | ||
| f"{base_url}/query", | ||
| json={ | ||
| "manifestStr": manifest_str, | ||
| "sql": 'SELECT order_id, customer_id, status, amount FROM "orders" ORDER BY order_id LIMIT 1', | ||
| "connectionInfo": connection_info, | ||
| }, | ||
| ) | ||
| assert response.status_code == 200 | ||
| result = response.json() | ||
| assert result["columns"] == ["order_id", "customer_id", "status", "amount"] | ||
| assert len(result["data"]) == 1 | ||
| assert result["data"][0] == [1, 1, "returned", 10.0] | ||
|
|
||
|
|
||
| async def test_dry_run(client, manifest_str, connection_info): | ||
| response = await client.post( | ||
| f"{base_url}/query", | ||
| params={"dryRun": True}, | ||
| json={ | ||
| "manifestStr": manifest_str, | ||
| "sql": 'SELECT * FROM "customers" LIMIT 1', | ||
| "connectionInfo": connection_info, | ||
| }, | ||
| ) | ||
| assert response.status_code == 204 | ||
|
|
||
| response = await client.post( | ||
| f"{base_url}/query", | ||
| params={"dryRun": True}, | ||
| json={ | ||
| "manifestStr": manifest_str, | ||
| "sql": 'SELECT * FROM "NotFound" LIMIT 1', | ||
| "connectionInfo": connection_info, | ||
| }, | ||
| ) | ||
| assert response.status_code == 422 | ||
| assert response.text is not None |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.