polars-bigquery 0.2.0__py3-none-any.whl

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 (39) hide show
  1. polars_bigquery/__init__.py +11 -0
  2. polars_bigquery/_read_bigquery.py +146 -0
  3. polars_bigquery/core/__init__.py +0 -0
  4. polars_bigquery/core/bigquery_rest.py +300 -0
  5. polars_bigquery/core/compiler/__init__.py +46 -0
  6. polars_bigquery/core/compiler/ir/__init__.py +104 -0
  7. polars_bigquery/core/compiler/ir/base.py +67 -0
  8. polars_bigquery/core/compiler/ir/boolean.py +41 -0
  9. polars_bigquery/core/compiler/ir/comparison.py +35 -0
  10. polars_bigquery/core/compiler/ir/list_.py +17 -0
  11. polars_bigquery/core/compiler/ir/numeric.py +42 -0
  12. polars_bigquery/core/compiler/ir/string.py +68 -0
  13. polars_bigquery/core/compiler/ir/temporal.py +29 -0
  14. polars_bigquery/core/compiler/parser/__init__.py +144 -0
  15. polars_bigquery/core/compiler/parser/base.py +622 -0
  16. polars_bigquery/core/compiler/parser/boolean.py +57 -0
  17. polars_bigquery/core/compiler/parser/comparison.py +53 -0
  18. polars_bigquery/core/compiler/parser/list_.py +237 -0
  19. polars_bigquery/core/compiler/parser/numeric.py +102 -0
  20. polars_bigquery/core/compiler/parser/string.py +77 -0
  21. polars_bigquery/core/compiler/parser/temporal.py +78 -0
  22. polars_bigquery/core/compiler/rewriter/__init__.py +25 -0
  23. polars_bigquery/core/compiler/rewriter/base.py +102 -0
  24. polars_bigquery/core/compiler/rewriter/boolean.py +26 -0
  25. polars_bigquery/core/compiler/sql/__init__.py +91 -0
  26. polars_bigquery/core/compiler/sql/base.py +159 -0
  27. polars_bigquery/core/compiler/sql/boolean.py +66 -0
  28. polars_bigquery/core/compiler/sql/comparison.py +41 -0
  29. polars_bigquery/core/compiler/sql/list_.py +57 -0
  30. polars_bigquery/core/compiler/sql/numeric.py +52 -0
  31. polars_bigquery/core/compiler/sql/string.py +63 -0
  32. polars_bigquery/core/compiler/sql/temporal.py +27 -0
  33. polars_bigquery/core/schema.py +111 -0
  34. polars_bigquery/core/version.py +1 -0
  35. polars_bigquery/exceptions.py +2 -0
  36. polars_bigquery-0.2.0.dist-info/METADATA +11 -0
  37. polars_bigquery-0.2.0.dist-info/RECORD +39 -0
  38. polars_bigquery-0.2.0.dist-info/WHEEL +5 -0
  39. polars_bigquery-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from polars_bigquery._read_bigquery import (
4
+ Client,
5
+ )
6
+ from polars_bigquery.core.version import __version__
7
+
8
+ __all__ = [
9
+ "Client",
10
+ "__version__",
11
+ ]
@@ -0,0 +1,146 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from typing import Any
5
+
6
+ import arrow_bigquery
7
+ import arrow_bigquery.api.resources
8
+ import polars as pl
9
+ import polars.io.plugins
10
+
11
+ import polars_bigquery.core.schema
12
+ import polars_bigquery.core.version
13
+ from polars_bigquery.core import bigquery_rest, compiler
14
+
15
+
16
+ def _get_user_agent(user_agent: str | None) -> str:
17
+ ua = f"polars-bigquery/{polars_bigquery.core.version.__version__}"
18
+
19
+ if user_agent:
20
+ return f"{ua} {user_agent}"
21
+ else:
22
+ return ua
23
+
24
+
25
+ class Client:
26
+ """Client for reading data from BigQuery into Polars.
27
+
28
+ Wraps an arrow_bigquery Client to keep connections open and reuse credentials across operations.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ *,
34
+ quota_project_id: str,
35
+ credentials_provider: pl.CredentialProviderGCP | None = None,
36
+ user_agent: str | None = None,
37
+ ) -> None:
38
+ if credentials_provider is None:
39
+ credentials_provider = pl.CredentialProviderGCP(
40
+ quota_project_id=quota_project_id
41
+ )
42
+ self._credentials_provider = credentials_provider
43
+ self._user_agent = _get_user_agent(user_agent)
44
+ self._quota_project_id = quota_project_id
45
+ self._rest_client = bigquery_rest.BigQueryRestClient(
46
+ quota_project_id=quota_project_id,
47
+ credentials_provider=credentials_provider,
48
+ user_agent=self._user_agent,
49
+ )
50
+ self._arrow_client = arrow_bigquery.Client(
51
+ quota_project_id=quota_project_id,
52
+ credentials_provider=credentials_provider,
53
+ user_agent=self._user_agent,
54
+ )
55
+
56
+ def close(self) -> None:
57
+ """Close underlying REST session resources."""
58
+ self._rest_client.close()
59
+
60
+ def __enter__(self) -> Client: # noqa: PYI034
61
+ return self
62
+
63
+ def __exit__(self, *exc_info: object) -> None:
64
+ self.close()
65
+
66
+ @property
67
+ def credentials_provider(self) -> pl.CredentialProviderGCP:
68
+ return self._credentials_provider
69
+
70
+ @property
71
+ def quota_project_id(self) -> str:
72
+ return self._quota_project_id
73
+
74
+ def read_table(
75
+ self,
76
+ table: Any,
77
+ *,
78
+ maintain_order: bool = False,
79
+ ) -> pl.DataFrame:
80
+ table_ref = arrow_bigquery.api.resources.parse_table_id(table)
81
+ arrow_stream_exporter = self._arrow_client.read_table(
82
+ table_ref,
83
+ maintain_order=maintain_order,
84
+ )
85
+ return pl.DataFrame(arrow_stream_exporter)
86
+
87
+ def read_query(
88
+ self,
89
+ query: str,
90
+ *,
91
+ maintain_order: bool = False,
92
+ ) -> pl.DataFrame:
93
+ table = self._rest_client.run_query(query)
94
+ table_ref = arrow_bigquery.api.resources.parse_table_id(table)
95
+ arrow_stream_exporter = self._arrow_client.read_table(
96
+ table_ref,
97
+ maintain_order=maintain_order,
98
+ )
99
+ return pl.DataFrame(arrow_stream_exporter)
100
+
101
+ def scan_table(
102
+ self,
103
+ table: Any,
104
+ ) -> pl.LazyFrame:
105
+ table_ref = arrow_bigquery.api.resources.parse_table_id(table)
106
+ table_metadata = self._rest_client.get_table_metadata(table_ref)
107
+ schema = polars_bigquery.core.schema.extract_polars_schema(table_metadata)
108
+
109
+ def source_generator(
110
+ with_columns: list[str] | None,
111
+ predicate: pl.Expr | None,
112
+ n_rows: int | None,
113
+ batch_size: int | None,
114
+ ) -> Iterator[pl.DataFrame]:
115
+ columns = with_columns if with_columns is not None else list(schema.keys())
116
+ predicate_cols = (
117
+ predicate.meta.root_names() if predicate is not None else []
118
+ )
119
+ selected_fields = list(dict.fromkeys([*columns, *predicate_cols]))
120
+
121
+ arrow_stream_exporter = self._arrow_client.read_table(
122
+ table_ref,
123
+ maintain_order=False,
124
+ selected_fields=selected_fields,
125
+ row_restriction=compiler.predicate_to_row_restriction(
126
+ predicate=predicate
127
+ )
128
+ if predicate is not None
129
+ else "",
130
+ )
131
+ lazyframe = pl.scan_arrow_c_stream(arrow_stream_exporter)
132
+
133
+ if predicate is not None:
134
+ lazyframe = lazyframe.filter(predicate)
135
+
136
+ lazyframe = lazyframe.select(columns)
137
+
138
+ if n_rows is not None:
139
+ lazyframe = lazyframe.limit(n_rows)
140
+
141
+ return lazyframe.collect_batches(chunk_size=batch_size)
142
+
143
+ return polars.io.plugins.register_io_source(
144
+ io_source=source_generator,
145
+ schema=schema,
146
+ )
File without changes
@@ -0,0 +1,300 @@
1
+ """Utilities for running a query in BigQuery and getting the results as a table.
2
+
3
+ TODO(tswast): Implement this in Rust using jobs.query and the
4
+ JOB_CREATION_OPTIONAL parameter to improve latency in small query results.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import contextlib
10
+ import time
11
+ import uuid
12
+ from typing import Any
13
+
14
+ import arrow_bigquery.api.resources
15
+ import polars as pl
16
+ import requests
17
+ from requests.adapters import HTTPAdapter
18
+ from urllib3.util.retry import Retry
19
+
20
+ import polars_bigquery.exceptions
21
+
22
+ _BIGQUERY_ENDPOINT = "https://bigquery.googleapis.com/bigquery/v2"
23
+ # Keep timeout at least as long as the server-side timeout defined at
24
+ # https://github.com/googleapis/googleapis/blob/4bcbf04e688ffddb8ec1a20a2349df32b9fcbd9c/google/cloud/bigquery/v2/bigquery_grpc_service_config.json#L170-L181
25
+ _CONNECT_TIMEOUT = 60.0
26
+ _READ_TIMEOUT = 250.0
27
+ DEFAULT_TIMEOUT: tuple[float, float] = (_CONNECT_TIMEOUT, _READ_TIMEOUT)
28
+ MAX_POLL_SECONDS: float = 21600.0
29
+
30
+
31
+ def create_resilient_session() -> requests.Session:
32
+ """Create a requests.Session with connection pooling and exponential backoff."""
33
+ session = requests.Session()
34
+ retry_strategy = Retry(
35
+ total=5,
36
+ backoff_factor=0.5,
37
+ status_forcelist=(429, 500, 502, 503, 504),
38
+ allowed_methods=frozenset(["GET", "POST"]),
39
+ respect_retry_after_header=True,
40
+ )
41
+ adapter = HTTPAdapter(
42
+ max_retries=retry_strategy,
43
+ pool_connections=10,
44
+ pool_maxsize=20,
45
+ )
46
+ session.mount("https://", adapter)
47
+ session.mount("http://", adapter)
48
+ return session
49
+
50
+
51
+ def _raise_for_bigquery_error(response: requests.Response) -> None:
52
+ """Raise BigQueryError with structured message on HTTP error."""
53
+ try:
54
+ response.raise_for_status()
55
+ except requests.exceptions.HTTPError as exc:
56
+ message = str(exc)
57
+ with contextlib.suppress(ValueError, KeyError, TypeError):
58
+ err_json = response.json()
59
+ if isinstance(err_json, dict) and "error" in err_json:
60
+ err_obj = err_json["error"]
61
+ if isinstance(err_obj, dict) and "message" in err_obj:
62
+ message = err_obj["message"]
63
+ raise polars_bigquery.exceptions.BigQueryError(message) from exc
64
+
65
+
66
+ def _get_table_metadata_url(
67
+ *, table_ref: arrow_bigquery.api.resources.BigQueryTableId
68
+ ) -> str:
69
+ return (
70
+ f"{_BIGQUERY_ENDPOINT}/projects/{table_ref.project_id}"
71
+ f"/datasets/{table_ref.dataset_id}/tables/{table_ref.table_id}"
72
+ "?fields=schema,timePartitioning"
73
+ )
74
+
75
+
76
+ def _get_jobs_insert_url(quota_project_id: str) -> str:
77
+ return f"{_BIGQUERY_ENDPOINT}/projects/{quota_project_id}/jobs"
78
+
79
+
80
+ def _get_jobs_insert_body(query: str, quota_project_id: str) -> dict:
81
+ return {
82
+ "jobReference": {
83
+ "projectId": quota_project_id,
84
+ "jobId": f"polars_bq_{uuid.uuid4().hex}",
85
+ },
86
+ "configuration": {
87
+ "query": {
88
+ "query": query,
89
+ "useLegacySql": False,
90
+ }
91
+ },
92
+ }
93
+
94
+
95
+ def _get_jobs_get_url(
96
+ job_id: str, quota_project_id: str, location: str | None = None
97
+ ) -> str:
98
+ url = f"{_BIGQUERY_ENDPOINT}/projects/{quota_project_id}/jobs/{job_id}"
99
+ if location:
100
+ url += f"?location={location}"
101
+ return url
102
+
103
+
104
+ def _get_query_results_url(
105
+ job_id: str, quota_project_id: str, location: str | None = None
106
+ ) -> str:
107
+ # Set maxResults=0 and timeoutMs=10000 to use this endpoint to wait for job completion.
108
+ url = (
109
+ f"{_BIGQUERY_ENDPOINT}/projects/{quota_project_id}/queries/{job_id}/results"
110
+ "?maxResults=0&timeoutMs=10000"
111
+ )
112
+ if location:
113
+ url += f"&location={location}"
114
+ return url
115
+
116
+
117
+ def _wait_for_job(
118
+ job_id: str,
119
+ quota_project_id: str,
120
+ credentials_provider: pl.CredentialProviderGCP,
121
+ user_agent: str,
122
+ *,
123
+ location: str | None = None,
124
+ session: requests.Session | None = None,
125
+ timeout: tuple[float, float] = DEFAULT_TIMEOUT,
126
+ max_poll_seconds: float = MAX_POLL_SECONDS,
127
+ ) -> dict:
128
+ http = session if session is not None else requests
129
+ start_time = time.monotonic()
130
+ while True:
131
+ if time.monotonic() - start_time > max_poll_seconds:
132
+ raise polars_bigquery.exceptions.BigQueryError(
133
+ f"Timed out waiting for BigQuery job {job_id} after {max_poll_seconds}s"
134
+ )
135
+ headers = _get_headers(
136
+ quota_project_id=quota_project_id,
137
+ credentials_provider=credentials_provider,
138
+ user_agent=user_agent,
139
+ )
140
+ response = http.get(
141
+ _get_jobs_get_url(job_id, quota_project_id, location=location),
142
+ headers=headers,
143
+ timeout=timeout,
144
+ )
145
+ _raise_for_bigquery_error(response)
146
+ job = response.json()
147
+
148
+ if job["status"]["state"] == "DONE":
149
+ if "errorResult" in job["status"]:
150
+ raise polars_bigquery.exceptions.BigQueryError(
151
+ job["status"]["errorResult"]["message"]
152
+ )
153
+ return job
154
+
155
+ # jobs.getQueryResults waits about 10s or until the query finishes.
156
+ poll_start = time.monotonic()
157
+ poll_response = http.get(
158
+ _get_query_results_url(job_id, quota_project_id, location=location),
159
+ headers=headers,
160
+ timeout=timeout,
161
+ )
162
+ _raise_for_bigquery_error(poll_response)
163
+ # Avoid busy-spinning if the polling endpoint returns without blocking
164
+ elapsed = time.monotonic() - poll_start
165
+ if elapsed < 1.0:
166
+ time.sleep(1.0 - elapsed)
167
+
168
+
169
+ def _get_headers(
170
+ *,
171
+ quota_project_id: str,
172
+ credentials_provider: pl.CredentialProviderGCP,
173
+ user_agent: str,
174
+ ) -> dict:
175
+ token_data, _ = credentials_provider()
176
+ token = token_data["bearer_token"]
177
+ return {
178
+ "Authorization": f"Bearer {token}",
179
+ "User-Agent": user_agent,
180
+ "Content-Type": "application/json",
181
+ "x-goog-user-project": quota_project_id,
182
+ }
183
+
184
+
185
+ def run_query(
186
+ query: str,
187
+ *,
188
+ quota_project_id: str,
189
+ credentials_provider: pl.CredentialProviderGCP,
190
+ user_agent: str,
191
+ session: requests.Session | None = None,
192
+ timeout: tuple[float, float] = DEFAULT_TIMEOUT,
193
+ ) -> str:
194
+ """Run a query and return the destination table from the job resource."""
195
+ http = session if session is not None else requests
196
+ headers = _get_headers(
197
+ quota_project_id=quota_project_id,
198
+ credentials_provider=credentials_provider,
199
+ user_agent=user_agent,
200
+ )
201
+
202
+ # 1. Insert the job
203
+ insert_url = _get_jobs_insert_url(quota_project_id)
204
+ body = _get_jobs_insert_body(query, quota_project_id)
205
+ response = http.post(insert_url, headers=headers, json=body, timeout=timeout)
206
+ _raise_for_bigquery_error(response)
207
+ job_resource = response.json()
208
+ job_ref = job_resource.get("jobReference", {})
209
+ job_id = job_ref["jobId"]
210
+ location = job_ref.get("location")
211
+
212
+ # 2. Wait for the job to complete (refreshing credentials on each poll)
213
+ job = _wait_for_job(
214
+ job_id,
215
+ quota_project_id,
216
+ credentials_provider,
217
+ user_agent,
218
+ location=location,
219
+ session=session,
220
+ timeout=timeout,
221
+ )
222
+
223
+ # 3. Return the destination table ID
224
+ dest = job["configuration"]["query"]["destinationTable"]
225
+ return f"{dest['projectId']}.{dest['datasetId']}.{dest['tableId']}"
226
+
227
+
228
+ def get_table_metadata(
229
+ table_ref: arrow_bigquery.api.resources.BigQueryTableId,
230
+ *,
231
+ quota_project_id: str,
232
+ credentials_provider: pl.CredentialProviderGCP,
233
+ user_agent: str,
234
+ session: requests.Session | None = None,
235
+ timeout: tuple[float, float] = DEFAULT_TIMEOUT,
236
+ ) -> dict:
237
+ http = session if session is not None else requests
238
+ headers = _get_headers(
239
+ quota_project_id=quota_project_id,
240
+ credentials_provider=credentials_provider,
241
+ user_agent=user_agent,
242
+ )
243
+ table_metadata_url = _get_table_metadata_url(table_ref=table_ref)
244
+ response = http.get(table_metadata_url, headers=headers, timeout=timeout)
245
+ _raise_for_bigquery_error(response)
246
+ return response.json()
247
+
248
+
249
+ class BigQueryRestClient:
250
+ """Stateful BigQuery REST client with connection pooling and metadata caching."""
251
+
252
+ def __init__(
253
+ self,
254
+ *,
255
+ quota_project_id: str,
256
+ credentials_provider: pl.CredentialProviderGCP,
257
+ user_agent: str,
258
+ session: requests.Session | None = None,
259
+ ) -> None:
260
+ self._quota_project_id = quota_project_id
261
+ self._credentials_provider = credentials_provider
262
+ self._user_agent = user_agent
263
+ self._owns_session = session is None
264
+ self._session = session if session is not None else create_resilient_session()
265
+
266
+ @property
267
+ def session(self) -> requests.Session:
268
+ return self._session
269
+
270
+ def close(self) -> None:
271
+ """Close the underlying HTTP session if owned by this client."""
272
+ if self._owns_session:
273
+ self._session.close()
274
+
275
+ def __enter__(self) -> BigQueryRestClient: # noqa: PYI034
276
+ return self
277
+
278
+ def __exit__(self, *exc_info: object) -> None:
279
+ self.close()
280
+
281
+ def run_query(self, query: str) -> str:
282
+ return run_query(
283
+ query,
284
+ quota_project_id=self._quota_project_id,
285
+ credentials_provider=self._credentials_provider,
286
+ user_agent=self._user_agent,
287
+ session=self._session,
288
+ )
289
+
290
+ def get_table_metadata(
291
+ self,
292
+ table_ref: arrow_bigquery.api.resources.BigQueryTableId,
293
+ ) -> dict[str, Any]:
294
+ return get_table_metadata(
295
+ table_ref,
296
+ quota_project_id=self._quota_project_id,
297
+ credentials_provider=self._credentials_provider,
298
+ user_agent=self._user_agent,
299
+ session=self._session,
300
+ )
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import json
5
+ from typing import Any
6
+
7
+ import polars as pl
8
+
9
+ from polars_bigquery.core.compiler import ir, parser, rewriter, sql
10
+ from polars_bigquery.core.compiler.parser import json_to_ir
11
+ from polars_bigquery.core.compiler.rewriter import rewrite_ir
12
+ from polars_bigquery.core.compiler.sql import ir_to_sql
13
+
14
+ __all__ = [
15
+ "ir",
16
+ "ir_to_sql",
17
+ "json_to_ir",
18
+ "parser",
19
+ "predicate_to_row_restriction",
20
+ "rewrite_ir",
21
+ "rewriter",
22
+ "sql",
23
+ ]
24
+
25
+
26
+ def _json_expr_to_row_restriction(expr_json: dict[str, Any]) -> str | None:
27
+ """Compile a Polars expression JSON dict into a BigQuery SQL row restriction.
28
+
29
+ Returns None if unknown operators are found and can't guarantee a superset of rows.
30
+ """
31
+ ir_tree = json_to_ir(expr_json)
32
+ ir_tree = rewrite_ir(ir_tree)
33
+ return ir_to_sql(ir_tree)
34
+
35
+
36
+ def predicate_to_row_restriction(predicate: pl.Expr) -> str:
37
+ """Orchestrate compilation of a Polars expression to a BigQuery row restriction SQL string."""
38
+ predicate_json_file = io.BytesIO()
39
+ predicate.meta.serialize(predicate_json_file, format="json")
40
+ predicate_json_file.seek(0)
41
+ try:
42
+ predicate_json = json.load(predicate_json_file)
43
+ row_restriction = _json_expr_to_row_restriction(predicate_json)
44
+ except (RecursionError, json.JSONDecodeError):
45
+ row_restriction = None
46
+ return row_restriction if row_restriction is not None else ""
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from polars_bigquery.core.compiler.ir import (
4
+ base,
5
+ boolean,
6
+ comparison,
7
+ list_,
8
+ numeric,
9
+ string,
10
+ temporal,
11
+ )
12
+ from polars_bigquery.core.compiler.ir.base import (
13
+ BinaryExpr,
14
+ Column,
15
+ Expr,
16
+ Literal,
17
+ NullLiteral,
18
+ UnaryExpr,
19
+ Unsupported,
20
+ )
21
+ from polars_bigquery.core.compiler.ir.boolean import (
22
+ And,
23
+ BoolLiteral,
24
+ IsNotNull,
25
+ IsNull,
26
+ Not,
27
+ Or,
28
+ )
29
+ from polars_bigquery.core.compiler.ir.comparison import (
30
+ Eq,
31
+ Gt,
32
+ GtEq,
33
+ Lt,
34
+ LtEq,
35
+ NotEq,
36
+ )
37
+ from polars_bigquery.core.compiler.ir.list_ import IsIn, ListLiteral
38
+ from polars_bigquery.core.compiler.ir.numeric import (
39
+ FloatLiteral,
40
+ IntLiteral,
41
+ IsFinite,
42
+ IsInfinite,
43
+ IsNan,
44
+ IsNotNan,
45
+ )
46
+ from polars_bigquery.core.compiler.ir.string import (
47
+ Contains,
48
+ EndsWith,
49
+ Lowercase,
50
+ StartsWith,
51
+ StringLiteral,
52
+ Uppercase,
53
+ )
54
+ from polars_bigquery.core.compiler.ir.temporal import (
55
+ DateLiteral,
56
+ TimestampLiteral,
57
+ TimestampUnit,
58
+ )
59
+
60
+ __all__ = [
61
+ "And",
62
+ "BinaryExpr",
63
+ "BoolLiteral",
64
+ "Column",
65
+ "Contains",
66
+ "DateLiteral",
67
+ "EndsWith",
68
+ "Eq",
69
+ "Expr",
70
+ "FloatLiteral",
71
+ "Gt",
72
+ "GtEq",
73
+ "IntLiteral",
74
+ "IsFinite",
75
+ "IsIn",
76
+ "IsInfinite",
77
+ "IsNan",
78
+ "IsNotNan",
79
+ "IsNotNull",
80
+ "IsNull",
81
+ "ListLiteral",
82
+ "Literal",
83
+ "Lowercase",
84
+ "Lt",
85
+ "LtEq",
86
+ "Not",
87
+ "NotEq",
88
+ "NullLiteral",
89
+ "Or",
90
+ "StartsWith",
91
+ "StringLiteral",
92
+ "TimestampLiteral",
93
+ "TimestampUnit",
94
+ "UnaryExpr",
95
+ "Unsupported",
96
+ "Uppercase",
97
+ "base",
98
+ "boolean",
99
+ "comparison",
100
+ "list_",
101
+ "numeric",
102
+ "string",
103
+ "temporal",
104
+ ]
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import abc
4
+ import dataclasses
5
+
6
+
7
+ @dataclasses.dataclass(frozen=True)
8
+ class Expr(abc.ABC):
9
+ """Base class for all predicate Intermediate Representation (IR) nodes."""
10
+
11
+ @abc.abstractmethod
12
+ def children(self) -> tuple[Expr, ...]:
13
+ """Return the child expressions of this node in order."""
14
+
15
+
16
+ @dataclasses.dataclass(frozen=True)
17
+ class UnaryExpr(Expr):
18
+ """Base class for unary operation IR nodes."""
19
+
20
+ expr: Expr
21
+
22
+ def children(self) -> tuple[Expr, ...]:
23
+ return (self.expr,)
24
+
25
+
26
+ @dataclasses.dataclass(frozen=True)
27
+ class BinaryExpr(Expr):
28
+ """Base class for binary operation IR nodes."""
29
+
30
+ left: Expr
31
+ right: Expr
32
+
33
+ def children(self) -> tuple[Expr, ...]:
34
+ return (self.left, self.right)
35
+
36
+
37
+ @dataclasses.dataclass(frozen=True)
38
+ class Unsupported(Expr):
39
+ """IR node representing an unsupported or unknown expression."""
40
+
41
+ operands: tuple[Expr, ...] = ()
42
+
43
+ def children(self) -> tuple[Expr, ...]:
44
+ return self.operands
45
+
46
+
47
+ @dataclasses.dataclass(frozen=True)
48
+ class Column(Expr):
49
+ """IR node representing a column reference."""
50
+
51
+ name: str
52
+
53
+ def children(self) -> tuple[Expr, ...]:
54
+ return ()
55
+
56
+
57
+ @dataclasses.dataclass(frozen=True)
58
+ class Literal(Expr):
59
+ """Base class for literal IR nodes."""
60
+
61
+ def children(self) -> tuple[Expr, ...]:
62
+ return ()
63
+
64
+
65
+ @dataclasses.dataclass(frozen=True)
66
+ class NullLiteral(Literal):
67
+ """IR node representing a NULL literal."""