graphique 2.0.2__tar.gz → 2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {graphique-2.0.2 → graphique-2.2}/PKG-INFO +43 -33
  2. {graphique-2.0.2 → graphique-2.2}/README.md +32 -25
  3. graphique-2.2/graphique/__init__.py +21 -0
  4. graphique-2.2/graphique/ducklake.py +41 -0
  5. {graphique-2.0.2 → graphique-2.2}/graphique/inputs.py +133 -71
  6. graphique-2.2/graphique/interface.py +578 -0
  7. graphique-2.2/graphique/middleware.py +169 -0
  8. {graphique-2.0.2 → graphique-2.2}/graphique/models.py +46 -52
  9. graphique-2.2/graphique/partition.py +62 -0
  10. {graphique-2.0.2 → graphique-2.2}/graphique/scalars.py +47 -3
  11. graphique-2.2/graphique/service.py +38 -0
  12. {graphique-2.0.2 → graphique-2.2}/graphique.egg-info/PKG-INFO +43 -33
  13. {graphique-2.0.2 → graphique-2.2}/graphique.egg-info/SOURCES.txt +2 -2
  14. {graphique-2.0.2 → graphique-2.2}/graphique.egg-info/requires.txt +6 -3
  15. {graphique-2.0.2 → graphique-2.2}/pyproject.toml +22 -19
  16. {graphique-2.0.2 → graphique-2.2}/tests/test_core.py +27 -15
  17. graphique-2.2/tests/test_dataset.py +177 -0
  18. {graphique-2.0.2 → graphique-2.2}/tests/test_models.py +25 -5
  19. {graphique-2.0.2 → graphique-2.2}/tests/test_service.py +100 -17
  20. graphique-2.0.2/graphique/__init__.py +0 -3
  21. graphique-2.0.2/graphique/core.py +0 -83
  22. graphique-2.0.2/graphique/interface.py +0 -383
  23. graphique-2.0.2/graphique/middleware.py +0 -125
  24. graphique-2.0.2/graphique/service.py +0 -44
  25. graphique-2.0.2/graphique/shell.py +0 -83
  26. graphique-2.0.2/tests/test_dataset.py +0 -167
  27. {graphique-2.0.2 → graphique-2.2}/LICENSE.txt +0 -0
  28. {graphique-2.0.2 → graphique-2.2}/graphique/py.typed +0 -0
  29. {graphique-2.0.2 → graphique-2.2}/graphique.egg-info/dependency_links.txt +0 -0
  30. {graphique-2.0.2 → graphique-2.2}/graphique.egg-info/top_level.txt +0 -0
  31. {graphique-2.0.2 → graphique-2.2}/setup.cfg +0 -0
@@ -1,41 +1,44 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphique
3
- Version: 2.0.2
3
+ Version: 2.2
4
4
  Summary: GraphQL service for python dataframes and parquet datasets.
5
- Author-email: Aric Coady <aric.coady@gmail.com>
5
+ Author: A. Coady
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/coady/graphique
8
8
  Project-URL: Documentation, https://coady.github.io/graphique
9
9
  Project-URL: Changelog, https://github.com/coady/graphique/blob/main/CHANGELOG.md
10
10
  Project-URL: Issues, https://github.com/coady/graphique/issues
11
- Keywords: graphql,arrow,parquet
11
+ Keywords: graphql,ibis,dataframes,arrow,parquet,duckdb
12
12
  Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: Intended Audience :: Science/Research
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.11
18
17
  Classifier: Programming Language :: Python :: 3.12
19
18
  Classifier: Programming Language :: Python :: 3.13
20
19
  Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: 3.15
21
21
  Classifier: Topic :: Database :: Database Engines/Servers
22
22
  Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
23
23
  Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
24
24
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
25
  Classifier: Typing :: Typed
26
- Requires-Python: >=3.11
26
+ Requires-Python: >=3.12
27
27
  Description-Content-Type: text/markdown
28
28
  License-File: LICENSE.txt
29
- Requires-Dist: pyarrow>=20
30
- Requires-Dist: strawberry-graphql[asgi,cli]>=0.288
29
+ Requires-Dist: pyarrow>=23
30
+ Requires-Dist: strawberry-graphql[asgi,cli]>=0.328.3
31
31
  Requires-Dist: ibis-framework>=10.7
32
- Requires-Dist: pyarrow-hotfix
33
32
  Requires-Dist: isodate>=0.7
33
+ Requires-Dist: pyarrow-hotfix
34
+ Requires-Dist: sqlglot<30.18
34
35
  Provides-Extra: server
35
36
  Requires-Dist: uvicorn[standard]; extra == "server"
36
37
  Requires-Dist: duckdb; extra == "server"
37
38
  Provides-Extra: cli
39
+ Requires-Dist: duckdb; extra == "cli"
38
40
  Requires-Dist: tqdm; extra == "cli"
41
+ Requires-Dist: typer; extra == "cli"
39
42
  Dynamic: license-file
40
43
 
41
44
  [![image](https://img.shields.io/pypi/v/graphique.svg)](https://pypi.org/project/graphique/)
@@ -48,14 +51,9 @@ Dynamic: license-file
48
51
  [![image](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
49
52
  [![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
50
53
 
51
- [GraphQL](https://graphql.org) service for [ibis](https://ibis-project.org) dataframes, [arrow](https://arrow.apache.org) tables, and [parquet](https://parquet.apache.org) datasets. The schema for a query API is derived automatically.
52
-
53
- ## Version 2
54
- When this project started, there was no out-of-core execution engine with performance comparable to [PyArrow](https://arrow.apache.org/docs/python/index.html). So it effectively included one, based on datasets and [Acero](https://arrow.apache.org/docs/python/api/acero.html).
54
+ [GraphQL](https://graphql.org) service for [ibis](https://ibis-project.org) tables. Ibis supports [20+ backends](https://ibis-project.org/why#how-does-ibis-work) — DuckDB, PostgreSQL, Polars, BigQuery, etc. — so the same query API works across local files and remote databases. The schema is derived automatically.
55
55
 
56
- Since then the ecosystem has grown considerably: [DuckDB](https://duckdb.org), [DataFusion](https://datafusion.apache.org), and [Ibis](https://ibis-project.org). As of version 2, graphique is based on `ibis`. It provides a common dataframe API for multiple backends, enabling graphique to also have a default but configurable backend.
57
-
58
- Being a major version upgrade, there are incompatible changes from version 1. However the overall API remains largely the same.
56
+ [Parquet](https://parquet.apache.org) files load with `ibis.read_parquet`. For hive-partitioned data, [DuckLake](https://ducklake.select/) (`graphique.ducklake.register` / `attach`) keeps partition metadata in a catalog so DuckDB can prune and count without scanning footers. Passing a PyArrow `Dataset` as a root is deprecated.
59
57
 
60
58
  ## Usage
61
59
  There is an example app which reads a parquet dataset.
@@ -74,25 +72,30 @@ outputs the graphql schema.
74
72
  The example app uses [Starlette's config](https://www.starlette.io/config/): in environment variables or a `.env` file.
75
73
 
76
74
  * PARQUET_PATH: path to the parquet directory or file
77
- * FEDERATED = '': field name to extend type `Query` with a federated `Table`
78
- * METRICS = False: include timings from apollo tracing extension
75
+ * NAME = '': GraphQL field on `Query`; defaults to root type
79
76
  * COLUMNS = None: list of names, or mapping of aliases, of columns to select
80
- * FILTERS = None: json `filter` query for which rows to read at startup
81
77
 
82
78
  Configuration options exist to provide a convenient no-code solution, but are subject to change in the future. Using a custom app is recommended for production usage.
83
79
 
84
80
  ### App
85
- For more options create a custom [ASGI](https://asgi.readthedocs.io/en/latest/index.html) app. Call graphique's `GraphQL` on an ibis [Table](https://ibis-project.org/reference/expression-tables) or arrow [Dataset](https://arrow.apache.org/docs/python/api/dataset.html).
86
- Supply a mapping of names to datasets for multiple roots, and to enable federation.
81
+ For more options create a custom [ASGI](https://asgi.readthedocs.io/en/latest/index.html) app. Call graphique's `GraphQL` on an ibis [Table](https://ibis-project.org/reference/expression-tables).
82
+ Use a `Query` type with table attributes for multiple roots, and to enable federation.
87
83
 
88
84
  ```python
89
85
  import ibis
90
- from graphique import GraphQL
86
+ from graphique import GraphQL, typed
87
+
88
+ # any ibis backend: DuckDB, PostgreSQL, Polars, BigQuery, ...
89
+ table = ibis.read_parquet(...) # or `ibis.connect(...).table(...)`
90
+ app = GraphQL(table) # Table is root query type
91
91
 
92
- source = ibis.read_*(...) # or ibis.connect(...).table(...) or pyarrow.dataset.dataset(...)
93
- # apply initial projections or filters to `source`
94
- app = GraphQL(source) # Table is root query type
95
- app = GraphQL.federated({<name>: source, ...}, keys={<name>: [], ...}) # Tables on federated fields
92
+
93
+ # multiple named fields, with optional federation keys
94
+ class Query:
95
+ name = table # or `typed(table, name, keys=...)`
96
+
97
+
98
+ app = GraphQL(Query)
96
99
  ```
97
100
 
98
101
  Start like any ASGI app.
@@ -103,7 +106,7 @@ uvicorn <module>:app
103
106
 
104
107
  ### API
105
108
  #### types
106
- * `Dataset`: interface for an ibis table or arrow dataset.
109
+ * `Dataset`: GraphQL interface over an ibis table.
107
110
  * `Table`: implements the `Dataset` interface. Adds typed `row`, `columns`, and `filter` fields from introspecting the schema.
108
111
  * `Column`: interface for an ibis column. Each data type has a corresponding column implementation: Boolean, Int, BigInt, Float, Decimal, Date, Datetime, Time, Duration, Base64, String, Array, Struct. All columns have a `values` field for their list of scalars. Additional fields vary by type.
109
112
  * `Row`: scalar fields. Tables are column-oriented, and graphique encourages that usage for performance. A single `row` field is provided for convenience, but a field for a list of rows is not. Requesting parallel columns is far more efficient.
@@ -111,7 +114,8 @@ uvicorn <module>:app
111
114
  #### selection
112
115
  * `slice`: contiguous selection of rows
113
116
  * `filter`: select rows by predicates
114
- * `join`: join tables by key columns
117
+ * `join`, `asofJoin`, `crossJoin`: join tables by key columns
118
+ * `difference`, `intersect`, `union`: set operations on tables
115
119
  * `take`: rows by index
116
120
  * `dropNull`: remove rows with nulls
117
121
 
@@ -121,27 +125,33 @@ uvicorn <module>:app
121
125
  * `column`: access a column of any type by name
122
126
  * `row`: provides a field for each scalar of a single row
123
127
  * `cast`: cast column types
128
+ * `unpack`: project struct fields
124
129
  * `fillNull`: fill null values
125
130
 
126
131
  #### aggregation
127
132
  * `group`: group by given columns, and aggregate the others
128
133
  * `distinct`: group with all columns
129
- * `runs`: provisionally group by adjacency
134
+ * `runs`: group by adjacency
130
135
  * `unnest`: unnest an array column
131
- * `count`: number of rows
136
+ * `count`, `any`: number of rows
132
137
 
133
138
  #### ordering
134
139
  * `order`: sort table by given columns
135
- * options `limit` and `dense`: select rows with smallest or largest values
140
+ * `first`: sort and filter by rank
141
+
142
+ #### reflection
143
+ * `schema`: field names and types
144
+ * `optional`: nullable for errors
145
+ * `toSql`: compiles SQL query
136
146
 
137
147
  ### Performance
138
- Performance is dependent on the [ibis backend](https://ibis-project.org/backends/duckdb), which defaults to [duckdb](https://duckdb.org/). There are no internal Python loops. Scalars do not become Python types until serialized.
148
+ Performance is dependent on the Ibis backend, which defaults to DuckDB. There are no internal Python loops. Scalars do not become Python types until serialized. Table fields are lazily evaluated up until scalars are reached, and automatically cached as needed for multiple fields.
139
149
 
140
- [PyArrow](https://arrow.apache.org/docs/python/) is also used for partitioned dataset optimizations, and for any feature which ibis does not support. Table fields are lazily evaluated up until scalars are reached, and automatically cached as needed for multiple fields.
150
+ For partitioned parquet datasets, [DuckLake](https://ducklake.select) provides performance benefits over `ibis.read_parquet`; see `graphique.ducklake.{register,attach}` for helpers. `python -m graphique.partition` in `graphique[cli]` writes hive-layout parquet out-of-core.
141
151
 
142
152
  ## Installation
143
153
  ```console
144
- pip install graphique[server]
154
+ pip install graphique[server,cli]
145
155
  ```
146
156
 
147
157
  ## Dependencies
@@ -8,14 +8,9 @@
8
8
  [![image](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
9
9
  [![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
10
10
 
11
- [GraphQL](https://graphql.org) service for [ibis](https://ibis-project.org) dataframes, [arrow](https://arrow.apache.org) tables, and [parquet](https://parquet.apache.org) datasets. The schema for a query API is derived automatically.
11
+ [GraphQL](https://graphql.org) service for [ibis](https://ibis-project.org) tables. Ibis supports [20+ backends](https://ibis-project.org/why#how-does-ibis-work) — DuckDB, PostgreSQL, Polars, BigQuery, etc. — so the same query API works across local files and remote databases. The schema is derived automatically.
12
12
 
13
- ## Version 2
14
- When this project started, there was no out-of-core execution engine with performance comparable to [PyArrow](https://arrow.apache.org/docs/python/index.html). So it effectively included one, based on datasets and [Acero](https://arrow.apache.org/docs/python/api/acero.html).
15
-
16
- Since then the ecosystem has grown considerably: [DuckDB](https://duckdb.org), [DataFusion](https://datafusion.apache.org), and [Ibis](https://ibis-project.org). As of version 2, graphique is based on `ibis`. It provides a common dataframe API for multiple backends, enabling graphique to also have a default but configurable backend.
17
-
18
- Being a major version upgrade, there are incompatible changes from version 1. However the overall API remains largely the same.
13
+ [Parquet](https://parquet.apache.org) files load with `ibis.read_parquet`. For hive-partitioned data, [DuckLake](https://ducklake.select/) (`graphique.ducklake.register` / `attach`) keeps partition metadata in a catalog so DuckDB can prune and count without scanning footers. Passing a PyArrow `Dataset` as a root is deprecated.
19
14
 
20
15
  ## Usage
21
16
  There is an example app which reads a parquet dataset.
@@ -34,25 +29,30 @@ outputs the graphql schema.
34
29
  The example app uses [Starlette's config](https://www.starlette.io/config/): in environment variables or a `.env` file.
35
30
 
36
31
  * PARQUET_PATH: path to the parquet directory or file
37
- * FEDERATED = '': field name to extend type `Query` with a federated `Table`
38
- * METRICS = False: include timings from apollo tracing extension
32
+ * NAME = '': GraphQL field on `Query`; defaults to root type
39
33
  * COLUMNS = None: list of names, or mapping of aliases, of columns to select
40
- * FILTERS = None: json `filter` query for which rows to read at startup
41
34
 
42
35
  Configuration options exist to provide a convenient no-code solution, but are subject to change in the future. Using a custom app is recommended for production usage.
43
36
 
44
37
  ### App
45
- For more options create a custom [ASGI](https://asgi.readthedocs.io/en/latest/index.html) app. Call graphique's `GraphQL` on an ibis [Table](https://ibis-project.org/reference/expression-tables) or arrow [Dataset](https://arrow.apache.org/docs/python/api/dataset.html).
46
- Supply a mapping of names to datasets for multiple roots, and to enable federation.
38
+ For more options create a custom [ASGI](https://asgi.readthedocs.io/en/latest/index.html) app. Call graphique's `GraphQL` on an ibis [Table](https://ibis-project.org/reference/expression-tables).
39
+ Use a `Query` type with table attributes for multiple roots, and to enable federation.
47
40
 
48
41
  ```python
49
42
  import ibis
50
- from graphique import GraphQL
43
+ from graphique import GraphQL, typed
44
+
45
+ # any ibis backend: DuckDB, PostgreSQL, Polars, BigQuery, ...
46
+ table = ibis.read_parquet(...) # or `ibis.connect(...).table(...)`
47
+ app = GraphQL(table) # Table is root query type
48
+
51
49
 
52
- source = ibis.read_*(...) # or ibis.connect(...).table(...) or pyarrow.dataset.dataset(...)
53
- # apply initial projections or filters to `source`
54
- app = GraphQL(source) # Table is root query type
55
- app = GraphQL.federated({<name>: source, ...}, keys={<name>: [], ...}) # Tables on federated fields
50
+ # multiple named fields, with optional federation keys
51
+ class Query:
52
+ name = table # or `typed(table, name, keys=...)`
53
+
54
+
55
+ app = GraphQL(Query)
56
56
  ```
57
57
 
58
58
  Start like any ASGI app.
@@ -63,7 +63,7 @@ uvicorn <module>:app
63
63
 
64
64
  ### API
65
65
  #### types
66
- * `Dataset`: interface for an ibis table or arrow dataset.
66
+ * `Dataset`: GraphQL interface over an ibis table.
67
67
  * `Table`: implements the `Dataset` interface. Adds typed `row`, `columns`, and `filter` fields from introspecting the schema.
68
68
  * `Column`: interface for an ibis column. Each data type has a corresponding column implementation: Boolean, Int, BigInt, Float, Decimal, Date, Datetime, Time, Duration, Base64, String, Array, Struct. All columns have a `values` field for their list of scalars. Additional fields vary by type.
69
69
  * `Row`: scalar fields. Tables are column-oriented, and graphique encourages that usage for performance. A single `row` field is provided for convenience, but a field for a list of rows is not. Requesting parallel columns is far more efficient.
@@ -71,7 +71,8 @@ uvicorn <module>:app
71
71
  #### selection
72
72
  * `slice`: contiguous selection of rows
73
73
  * `filter`: select rows by predicates
74
- * `join`: join tables by key columns
74
+ * `join`, `asofJoin`, `crossJoin`: join tables by key columns
75
+ * `difference`, `intersect`, `union`: set operations on tables
75
76
  * `take`: rows by index
76
77
  * `dropNull`: remove rows with nulls
77
78
 
@@ -81,27 +82,33 @@ uvicorn <module>:app
81
82
  * `column`: access a column of any type by name
82
83
  * `row`: provides a field for each scalar of a single row
83
84
  * `cast`: cast column types
85
+ * `unpack`: project struct fields
84
86
  * `fillNull`: fill null values
85
87
 
86
88
  #### aggregation
87
89
  * `group`: group by given columns, and aggregate the others
88
90
  * `distinct`: group with all columns
89
- * `runs`: provisionally group by adjacency
91
+ * `runs`: group by adjacency
90
92
  * `unnest`: unnest an array column
91
- * `count`: number of rows
93
+ * `count`, `any`: number of rows
92
94
 
93
95
  #### ordering
94
96
  * `order`: sort table by given columns
95
- * options `limit` and `dense`: select rows with smallest or largest values
97
+ * `first`: sort and filter by rank
98
+
99
+ #### reflection
100
+ * `schema`: field names and types
101
+ * `optional`: nullable for errors
102
+ * `toSql`: compiles SQL query
96
103
 
97
104
  ### Performance
98
- Performance is dependent on the [ibis backend](https://ibis-project.org/backends/duckdb), which defaults to [duckdb](https://duckdb.org/). There are no internal Python loops. Scalars do not become Python types until serialized.
105
+ Performance is dependent on the Ibis backend, which defaults to DuckDB. There are no internal Python loops. Scalars do not become Python types until serialized. Table fields are lazily evaluated up until scalars are reached, and automatically cached as needed for multiple fields.
99
106
 
100
- [PyArrow](https://arrow.apache.org/docs/python/) is also used for partitioned dataset optimizations, and for any feature which ibis does not support. Table fields are lazily evaluated up until scalars are reached, and automatically cached as needed for multiple fields.
107
+ For partitioned parquet datasets, [DuckLake](https://ducklake.select) provides performance benefits over `ibis.read_parquet`; see `graphique.ducklake.{register,attach}` for helpers. `python -m graphique.partition` in `graphique[cli]` writes hive-layout parquet out-of-core.
101
108
 
102
109
  ## Installation
103
110
  ```console
104
- pip install graphique[server]
111
+ pip install graphique[server,cli]
105
112
  ```
106
113
 
107
114
  ## Dependencies
@@ -0,0 +1,21 @@
1
+ from .interface import Dataset
2
+ from .middleware import GraphQL, MetricsExtension, implement, typed
3
+
4
+
5
+ def fetch_record_batch(self, rows_per_batch):
6
+ return self.to_arrow_reader(rows_per_batch)
7
+
8
+
9
+ def connect(database=":memory:", read_only=False, config={}):
10
+ return original(database, read_only, {"lambda_syntax": "ENABLE_SINGLE_ARROW", **config})
11
+
12
+
13
+ try:
14
+ import duckdb
15
+ except ImportError: # pragma: no cover
16
+ pass
17
+ else: # ibis-project/ibis#12196
18
+ if not hasattr(duckdb.DuckDBPyConnection, "fetch_arrow_table"): # pragma: no branch
19
+ duckdb.DuckDBPyConnection.fetch_arrow_table = duckdb.DuckDBPyConnection.to_arrow_table
20
+ duckdb.DuckDBPyConnection.fetch_record_batch = fetch_record_batch
21
+ original, duckdb.connect = duckdb.connect, connect # type: ignore
@@ -0,0 +1,41 @@
1
+ """DuckLake helpers."""
2
+
3
+ import functools
4
+ from collections.abc import Sequence
5
+
6
+ import ibis
7
+ import pyarrow.dataset as ds
8
+
9
+
10
+ @functools.singledispatch
11
+ def register(
12
+ source: str | Sequence[str],
13
+ table: str,
14
+ *partitioning: str,
15
+ catalog: str = ":memory:",
16
+ database: str = "lake.main",
17
+ ) -> ibis.Table:
18
+ """Attach a DuckLake catalog and register a parquet dataset."""
19
+ alias, _, schema = database.partition(".")
20
+ con = ibis.duckdb.connect(extensions=["ducklake"])
21
+ con.raw_sql(f"ATTACH 'ducklake:{catalog}' AS {alias}")
22
+ con.create_table(table, schema=con.read_parquet(source).schema(), database=database)
23
+ if columns := ", ".join(partitioning):
24
+ con.raw_sql(f"ALTER TABLE {database}.{table} SET PARTITIONED BY ({columns})")
25
+ parameters = alias, table, source, schema
26
+ con.raw_sql("CALL ducklake_add_data_files(?, ?, ?, schema => ?)", parameters=parameters)
27
+ return con.table(table, database=database)
28
+
29
+
30
+ @register.register
31
+ def _(source: ds.Dataset, table: str, filter: ds.Expression | None = None, **kwargs) -> ibis.Table:
32
+ paths = [frag.path for frag in source.get_fragments(filter)]
33
+ return register(paths, table, *source.partitioning.schema.names, **kwargs)
34
+
35
+
36
+ def attach(table: str, catalog: str, database: str = "lake.main") -> ibis.Table:
37
+ """Attach a DuckLake catalog read-only and return a table."""
38
+ alias, _, _ = database.partition(".")
39
+ con = ibis.duckdb.connect(extensions=["ducklake"])
40
+ con.raw_sql(f"ATTACH 'ducklake:{catalog}' AS {alias} (READ_ONLY)")
41
+ return con.table(table, database=database)