graphique 2.1__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 (29) hide show
  1. {graphique-2.1 → graphique-2.2}/PKG-INFO +24 -25
  2. {graphique-2.1 → graphique-2.2}/README.md +16 -19
  3. graphique-2.2/graphique/__init__.py +21 -0
  4. graphique-2.2/graphique/ducklake.py +41 -0
  5. {graphique-2.1 → graphique-2.2}/graphique/inputs.py +23 -65
  6. {graphique-2.1 → graphique-2.2}/graphique/interface.py +169 -136
  7. {graphique-2.1 → graphique-2.2}/graphique/middleware.py +29 -27
  8. {graphique-2.1 → graphique-2.2}/graphique/models.py +17 -18
  9. graphique-2.2/graphique/partition.py +62 -0
  10. {graphique-2.1 → graphique-2.2}/graphique/scalars.py +19 -1
  11. graphique-2.2/graphique/service.py +38 -0
  12. {graphique-2.1 → graphique-2.2}/graphique.egg-info/PKG-INFO +24 -25
  13. {graphique-2.1 → graphique-2.2}/graphique.egg-info/SOURCES.txt +1 -1
  14. {graphique-2.1 → graphique-2.2}/graphique.egg-info/requires.txt +4 -2
  15. {graphique-2.1 → graphique-2.2}/pyproject.toml +15 -11
  16. {graphique-2.1 → graphique-2.2}/tests/test_core.py +13 -33
  17. graphique-2.2/tests/test_dataset.py +177 -0
  18. {graphique-2.1 → graphique-2.2}/tests/test_models.py +1 -1
  19. {graphique-2.1 → graphique-2.2}/tests/test_service.py +46 -17
  20. graphique-2.1/graphique/__init__.py +0 -2
  21. graphique-2.1/graphique/core.py +0 -100
  22. graphique-2.1/graphique/partition.py +0 -81
  23. graphique-2.1/graphique/service.py +0 -35
  24. graphique-2.1/tests/test_dataset.py +0 -218
  25. {graphique-2.1 → graphique-2.2}/LICENSE.txt +0 -0
  26. {graphique-2.1 → graphique-2.2}/graphique/py.typed +0 -0
  27. {graphique-2.1 → graphique-2.2}/graphique.egg-info/dependency_links.txt +0 -0
  28. {graphique-2.1 → graphique-2.2}/graphique.egg-info/top_level.txt +0 -0
  29. {graphique-2.1 → graphique-2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphique
3
- Version: 2.1
3
+ Version: 2.2
4
4
  Summary: GraphQL service for python dataframes and parquet datasets.
5
5
  Author: A. Coady
6
6
  License-Expression: Apache-2.0
@@ -8,33 +8,35 @@ 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,ibis,dataframes,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
29
  Requires-Dist: pyarrow>=23
30
- Requires-Dist: strawberry-graphql[asgi,cli]>=0.288
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"
39
41
  Requires-Dist: typer; extra == "cli"
40
42
  Dynamic: license-file
@@ -49,14 +51,9 @@ Dynamic: license-file
49
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)
50
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)
51
53
 
52
- [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.
53
-
54
- ## Version 2
55
- 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).
56
-
57
- 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.
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.
58
55
 
59
- 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.
60
57
 
61
58
  ## Usage
62
59
  There is an example app which reads a parquet dataset.
@@ -81,20 +78,23 @@ The example app uses [Starlette's config](https://www.starlette.io/config/): in
81
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.
82
79
 
83
80
  ### App
84
- 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).
85
- Use a `Query` type with dataset attributes 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.
86
83
 
87
84
  ```python
88
85
  import ibis
89
86
  from graphique import GraphQL, typed
90
87
 
91
- source = ibis.read_*(...) # or `ibis.connect(...).table(...)` or `pyarrow.dataset.dataset(...)`
92
- # apply initial projections or filters to `source`
93
- app = GraphQL(source) # Table is root query type
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
+
94
92
 
95
93
  # multiple named fields, with optional federation keys
96
94
  class Query:
97
- name = source # or `typed(source, name, keys=...)`
95
+ name = table # or `typed(table, name, keys=...)`
96
+
97
+
98
98
  app = GraphQL(Query)
99
99
  ```
100
100
 
@@ -106,7 +106,7 @@ uvicorn <module>:app
106
106
 
107
107
  ### API
108
108
  #### types
109
- * `Dataset`: interface for an ibis table or arrow dataset.
109
+ * `Dataset`: GraphQL interface over an ibis table.
110
110
  * `Table`: implements the `Dataset` interface. Adds typed `row`, `columns`, and `filter` fields from introspecting the schema.
111
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.
112
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.
@@ -131,24 +131,23 @@ uvicorn <module>:app
131
131
  #### aggregation
132
132
  * `group`: group by given columns, and aggregate the others
133
133
  * `distinct`: group with all columns
134
- * `runs`: provisionally group by adjacency
134
+ * `runs`: group by adjacency
135
135
  * `unnest`: unnest an array column
136
136
  * `count`, `any`: number of rows
137
137
 
138
138
  #### ordering
139
139
  * `order`: sort table by given columns
140
- * `first`: provisionally sort and filter by rank
140
+ * `first`: sort and filter by rank
141
141
 
142
142
  #### reflection
143
- * `type`: type of data source
144
143
  * `schema`: field names and types
145
144
  * `optional`: nullable for errors
146
145
  * `toSql`: compiles SQL query
147
146
 
148
147
  ### Performance
149
- 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. Table fields are lazily evaluated up until scalars are reached, and automatically cached as needed for multiple fields.
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.
150
149
 
151
- [PyArrow](https://arrow.apache.org/docs/python/) is also used for partitioned dataset optimizations. `python -m graphique.partition` is a command-line script provided in `graphique[cli]`, for out-of-core partitioning.
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.
152
151
 
153
152
  ## Installation
154
153
  ```console
@@ -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.
@@ -40,20 +35,23 @@ The example app uses [Starlette's config](https://www.starlette.io/config/): in
40
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.
41
36
 
42
37
  ### App
43
- 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).
44
- Use a `Query` type with dataset attributes 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.
45
40
 
46
41
  ```python
47
42
  import ibis
48
43
  from graphique import GraphQL, typed
49
44
 
50
- source = ibis.read_*(...) # or `ibis.connect(...).table(...)` or `pyarrow.dataset.dataset(...)`
51
- # apply initial projections or filters to `source`
52
- app = GraphQL(source) # Table is root query type
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
+
53
49
 
54
50
  # multiple named fields, with optional federation keys
55
51
  class Query:
56
- name = source # or `typed(source, name, keys=...)`
52
+ name = table # or `typed(table, name, keys=...)`
53
+
54
+
57
55
  app = GraphQL(Query)
58
56
  ```
59
57
 
@@ -65,7 +63,7 @@ uvicorn <module>:app
65
63
 
66
64
  ### API
67
65
  #### types
68
- * `Dataset`: interface for an ibis table or arrow dataset.
66
+ * `Dataset`: GraphQL interface over an ibis table.
69
67
  * `Table`: implements the `Dataset` interface. Adds typed `row`, `columns`, and `filter` fields from introspecting the schema.
70
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.
71
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.
@@ -90,24 +88,23 @@ uvicorn <module>:app
90
88
  #### aggregation
91
89
  * `group`: group by given columns, and aggregate the others
92
90
  * `distinct`: group with all columns
93
- * `runs`: provisionally group by adjacency
91
+ * `runs`: group by adjacency
94
92
  * `unnest`: unnest an array column
95
93
  * `count`, `any`: number of rows
96
94
 
97
95
  #### ordering
98
96
  * `order`: sort table by given columns
99
- * `first`: provisionally sort and filter by rank
97
+ * `first`: sort and filter by rank
100
98
 
101
99
  #### reflection
102
- * `type`: type of data source
103
100
  * `schema`: field names and types
104
101
  * `optional`: nullable for errors
105
102
  * `toSql`: compiles SQL query
106
103
 
107
104
  ### Performance
108
- 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. Table fields are lazily evaluated up until scalars are reached, and automatically cached as needed for multiple fields.
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.
109
106
 
110
- [PyArrow](https://arrow.apache.org/docs/python/) is also used for partitioned dataset optimizations. `python -m graphique.partition` is a command-line script provided in `graphique[cli]`, for out-of-core partitioning.
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.
111
108
 
112
109
  ## Installation
113
110
  ```console
@@ -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)
@@ -4,18 +4,14 @@ GraphQL input types.
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- import functools
8
7
  import inspect
9
8
  import operator
10
9
  from collections.abc import Callable, Iterable, Iterator
11
10
  from datetime import date, datetime, time
12
11
  from decimal import Decimal
13
- from typing import Generic, TypeVar
12
+ from typing import TypeVar
14
13
 
15
14
  import ibis.expr.types
16
- import pyarrow as pa
17
- import pyarrow.compute as pc
18
- import pyarrow.dataset as ds
19
15
  import strawberry
20
16
  from strawberry import UNSET
21
17
  from strawberry.annotation import StrawberryAnnotation
@@ -23,10 +19,9 @@ from strawberry.scalars import JSON
23
19
  from strawberry.schema_directive import Location
24
20
  from strawberry.types.arguments import StrawberryArgument
25
21
 
26
- from .core import getitems, links, order_key
27
- from .scalars import Duration
22
+ from .scalars import Duration, getitems, links, order_key
28
23
 
29
- T = TypeVar("T")
24
+ T = TypeVar("T") # needed for strawberry annotation evaluation
30
25
 
31
26
 
32
27
  def use_doc(decorator: Callable, **kwargs):
@@ -43,10 +38,7 @@ class optional:
43
38
  """
44
39
 
45
40
 
46
- @use_doc(
47
- strawberry.schema_directive,
48
- locations=[Location.ARGUMENT_DEFINITION, Location.FIELD_DEFINITION, Location.OBJECT],
49
- )
41
+ @use_doc(strawberry.schema_directive, locations=[Location.OBJECT])
50
42
  class provisional:
51
43
  """Provisional feature; subject to change in the future."""
52
44
 
@@ -67,7 +59,7 @@ class Field:
67
59
 
68
60
 
69
61
  @strawberry.input(description="predicates for scalars")
70
- class Filter(Generic[T]):
62
+ class Filter[T]:
71
63
  eq: list[T] | None = default_field(description="== or `isin`", nullable=True)
72
64
  ne: list[T] | None = default_field(description="!= or `notin`", nullable=True)
73
65
  lt: T | None = default_field(description="<")
@@ -76,7 +68,7 @@ class Filter(Generic[T]):
76
68
  ge: T | None = default_field(description=r"\>=")
77
69
 
78
70
  def __iter__(self) -> Iterator[tuple]:
79
- for name, value in self.__dict__.items():
71
+ for name, value in vars(self).items():
80
72
  if value is not UNSET:
81
73
  yield name, value
82
74
 
@@ -89,15 +81,15 @@ class Filter(Generic[T]):
89
81
  else:
90
82
  annotation = cls[scalar] # type: ignore
91
83
  yield StrawberryArgument(name, name, StrawberryAnnotation(annotation), default={})
92
- annotation = StrawberryAnnotation(Expression | None)
93
- yield StrawberryArgument("where", None, annotation, default=None)
84
+ annotation = StrawberryAnnotation(list[Expression])
85
+ yield StrawberryArgument("where", None, annotation, default=[])
94
86
 
95
87
  @staticmethod
96
- def to_exprs(**queries: Filter) -> Iterable[ibis.Deferred]:
88
+ def to_exprs(**predicates: Filter) -> Iterable[ibis.Deferred]:
97
89
  """Transform query syntax into ibis expressions."""
98
- for name, query in queries.items():
90
+ for name, predicate in predicates.items():
99
91
  field = ibis._[name]
100
- for op, value in query:
92
+ for op, value in predicate:
101
93
  match value, op:
102
94
  case list(), "eq":
103
95
  yield field == value[0] if len(value) == 1 else field.isin(value)
@@ -108,25 +100,9 @@ class Filter(Generic[T]):
108
100
  case _:
109
101
  yield getattr(operator, op)(field, value)
110
102
 
111
- @staticmethod
112
- def to_arrow(**queries: Filter | dict) -> ds.Expression | None:
113
- """Transform query syntax into an arrow expression."""
114
- exprs = []
115
- for name, query in queries.items():
116
- field = pc.field(name)
117
- for op, value in dict(query).items():
118
- if isinstance(value, list):
119
- expr = pc.is_in(field, pa.array(value))
120
- exprs.append(pc.invert(expr) if op == "ne" else expr)
121
- elif op == "contains": # placeholder, will not be in partition schema
122
- exprs.append(field == value)
123
- else:
124
- exprs.append(getattr(operator, op)(field, value))
125
- return functools.reduce(operator.and_, exprs or [None])
126
-
127
103
 
128
104
  @strawberry.input(description="predicates for arrays")
129
- class ArrayFilter(Generic[T]):
105
+ class ArrayFilter[T]:
130
106
  contains: T | None = default_field(description="array contains element")
131
107
 
132
108
  __iter__ = Filter.__iter__
@@ -228,8 +204,8 @@ class Aggregates:
228
204
  sum: list[Aggregate] = default_field([], func=ibis.expr.types.NumericColumn.sum)
229
205
  var: list[VarAggregate] = default_field([], func=ibis.expr.types.NumericColumn.var)
230
206
 
231
- def __iter__(self) -> Iterable[tuple]:
232
- for name, aggs in self.__dict__.items():
207
+ def __iter__(self) -> Iterator[tuple]:
208
+ for name, aggs in vars(self).items():
233
209
  for agg in aggs:
234
210
  yield agg.to_ibis(name)
235
211
 
@@ -244,7 +220,7 @@ class Scalars:
244
220
  time: time | None = default_field(description="time scalar")
245
221
 
246
222
  def __iter__(self):
247
- for value in self.__dict__.values():
223
+ for value in vars(self).values():
248
224
  if value is not UNSET:
249
225
  yield value
250
226
 
@@ -254,9 +230,6 @@ class Expression:
254
230
  name: list[str] = default_field([], description="column name(s)")
255
231
  value: JSON | None = default_field(description="JSON scalar", nullable=True)
256
232
  scalar: Scalars | None = default_field(description="typed scalar")
257
- row_number: None = default_field(
258
- func=ibis.row_number, deprecation_reason="use `window.rowNumber`"
259
- )
260
233
 
261
234
  eq: list[Expression] = default_field([], description="==")
262
235
  ne: list[Expression] = default_field([], description="!=")
@@ -278,21 +251,9 @@ class Expression:
278
251
  truediv: list[Expression] = default_field([], name="div", description="/")
279
252
 
280
253
  coalesce: list[Expression] = default_field([], func=ibis.Column.coalesce)
281
- cume_dist: Expression | None = default_field(
282
- func=ibis.Column.cume_dist, deprecation_reason="use `window.cumeDist`"
283
- )
284
254
  cummax: Expression | None = default_field(func=ibis.Column.cummax)
285
255
  cummin: Expression | None = default_field(func=ibis.Column.cummin)
286
- dense_rank: Expression | None = default_field(
287
- func=ibis.Column.dense_rank, deprecation_reason="use `window.denseRank`"
288
- )
289
256
  ifelse: list[Expression] = default_field([], func=ibis.expr.types.BooleanColumn.ifelse)
290
- percent_rank: Expression | None = default_field(
291
- func=ibis.Column.percent_rank, deprecation_reason="use `window.percentRank`"
292
- )
293
- rank: Expression | None = default_field(
294
- func=ibis.Column.rank, deprecation_reason="use `window.rank`"
295
- )
296
257
 
297
258
  array: Arrays | None = default_field(description="array value functions")
298
259
  numeric: Numeric | None = default_field(description="numeric functions")
@@ -301,21 +262,19 @@ class Expression:
301
262
  window: Window | None = default_field(description="window functions")
302
263
 
303
264
  def items(self) -> Iterable[tuple]:
304
- for name, value in self.__dict__.items():
265
+ for name, value in vars(self).items():
305
266
  if isinstance(value, Expression):
306
267
  yield name, [value.to_ibis()]
307
268
  elif isinstance(value, list) and value and isinstance(value[0], Expression):
308
269
  yield name, map(Expression.to_ibis, value)
309
270
 
310
- def __iter__(self) -> Iterable[ibis.Deferred]:
271
+ def __iter__(self):
311
272
  if self.name:
312
273
  yield getitems(ibis._, *self.name)
313
274
  if self.value is not UNSET:
314
275
  yield self.value
315
276
  if self.scalar:
316
277
  yield from self.scalar
317
- if self.row_number is not UNSET:
318
- yield ibis.row_number()
319
278
  for name, (expr, *args) in self.items():
320
279
  match name:
321
280
  case "eq" | "ne" | "lt" | "le" | "gt" | "ge" | "inv" | "and_" | "or_" | "xor":
@@ -328,11 +287,11 @@ class Expression:
328
287
  if field:
329
288
  yield from field
330
289
 
331
- def to_ibis(self) -> ibis.Deferred | None:
290
+ def to_ibis(self) -> ibis.Deferred:
332
291
  fields = list(self)
333
- if len(fields) > 1:
334
- raise ValueError("conflicting inputs: " + ", ".join(map(str, fields)))
335
- return next(iter(fields), None) # type: ignore
292
+ if len(fields) == 1:
293
+ return fields[0]
294
+ raise ValueError(f"'Expression' must specify one key (found {len(fields)})")
336
295
 
337
296
 
338
297
  @strawberry.input(description="an `Expression` with an optional alias")
@@ -340,10 +299,9 @@ class Projection(Expression):
340
299
  alias: str = strawberry.field(default="", description="name of projected column")
341
300
 
342
301
  def to_ibis(self) -> tuple: # type: ignore
343
- name = self.alias or ".".join(self.name)
344
- if name:
302
+ if name := self.alias or ".".join(self.name):
345
303
  return name, super().to_ibis()
346
- raise ValueError(f"projected fields require a name or alias: {self}")
304
+ raise ValueError("'Projection' must specify a name or alias")
347
305
 
348
306
 
349
307
  @strawberry.input(description=f"array [expressions]({links.ref}/expression-collections)")