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.
- polars_bigquery/__init__.py +11 -0
- polars_bigquery/_read_bigquery.py +146 -0
- polars_bigquery/core/__init__.py +0 -0
- polars_bigquery/core/bigquery_rest.py +300 -0
- polars_bigquery/core/compiler/__init__.py +46 -0
- polars_bigquery/core/compiler/ir/__init__.py +104 -0
- polars_bigquery/core/compiler/ir/base.py +67 -0
- polars_bigquery/core/compiler/ir/boolean.py +41 -0
- polars_bigquery/core/compiler/ir/comparison.py +35 -0
- polars_bigquery/core/compiler/ir/list_.py +17 -0
- polars_bigquery/core/compiler/ir/numeric.py +42 -0
- polars_bigquery/core/compiler/ir/string.py +68 -0
- polars_bigquery/core/compiler/ir/temporal.py +29 -0
- polars_bigquery/core/compiler/parser/__init__.py +144 -0
- polars_bigquery/core/compiler/parser/base.py +622 -0
- polars_bigquery/core/compiler/parser/boolean.py +57 -0
- polars_bigquery/core/compiler/parser/comparison.py +53 -0
- polars_bigquery/core/compiler/parser/list_.py +237 -0
- polars_bigquery/core/compiler/parser/numeric.py +102 -0
- polars_bigquery/core/compiler/parser/string.py +77 -0
- polars_bigquery/core/compiler/parser/temporal.py +78 -0
- polars_bigquery/core/compiler/rewriter/__init__.py +25 -0
- polars_bigquery/core/compiler/rewriter/base.py +102 -0
- polars_bigquery/core/compiler/rewriter/boolean.py +26 -0
- polars_bigquery/core/compiler/sql/__init__.py +91 -0
- polars_bigquery/core/compiler/sql/base.py +159 -0
- polars_bigquery/core/compiler/sql/boolean.py +66 -0
- polars_bigquery/core/compiler/sql/comparison.py +41 -0
- polars_bigquery/core/compiler/sql/list_.py +57 -0
- polars_bigquery/core/compiler/sql/numeric.py +52 -0
- polars_bigquery/core/compiler/sql/string.py +63 -0
- polars_bigquery/core/compiler/sql/temporal.py +27 -0
- polars_bigquery/core/schema.py +111 -0
- polars_bigquery/core/version.py +1 -0
- polars_bigquery/exceptions.py +2 -0
- polars_bigquery-0.2.0.dist-info/METADATA +11 -0
- polars_bigquery-0.2.0.dist-info/RECORD +39 -0
- polars_bigquery-0.2.0.dist-info/WHEEL +5 -0
- polars_bigquery-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -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."""
|