equiframe 0.12.4__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.
- equiframe/__init__.py +832 -0
- equiframe/__main__.py +3 -0
- equiframe/_base.py +84 -0
- equiframe/_cli_support.py +125 -0
- equiframe/_comparison_io.py +161 -0
- equiframe/_dataset_provenance.py +146 -0
- equiframe/_decimal.py +22 -0
- equiframe/_diff_capture.py +433 -0
- equiframe/_doctor.py +123 -0
- equiframe/_document_io.py +61 -0
- equiframe/_documents.py +79 -0
- equiframe/_duckdb_transport.py +149 -0
- equiframe/_error_reporting.py +52 -0
- equiframe/_first_use.py +102 -0
- equiframe/_path_compat.py +26 -0
- equiframe/_physical.py +133 -0
- equiframe/_r_review.R +308 -0
- equiframe/_regular_io.py +167 -0
- equiframe/_report_format_v1.py +298 -0
- equiframe/_report_io.py +591 -0
- equiframe/_stability_inventory.py +1421 -0
- equiframe/_timezones.py +257 -0
- equiframe/_vendor/cyclonedx/LICENSE +201 -0
- equiframe/_vendor/cyclonedx/PROVENANCE.md +18 -0
- equiframe/_vendor/cyclonedx/bom-1.6.schema.json +5673 -0
- equiframe/_vendor/cyclonedx/jsf-0.82.schema.json +240 -0
- equiframe/_vendor/cyclonedx/spdx.schema.json +712 -0
- equiframe/adapters.py +454 -0
- equiframe/assertion_types.py +122 -0
- equiframe/assertions.py +271 -0
- equiframe/benchmark.py +101 -0
- equiframe/bundle.py +240 -0
- equiframe/cli.py +693 -0
- equiframe/comparison.py +937 -0
- equiframe/comparison_report.py +660 -0
- equiframe/comparison_types.py +257 -0
- equiframe/compatibility.py +546 -0
- equiframe/contracts.py +879 -0
- equiframe/datasets.py +1301 -0
- equiframe/decision.py +34 -0
- equiframe/destinations.py +295 -0
- equiframe/detector_types.py +431 -0
- equiframe/detectors.py +389 -0
- equiframe/diff.py +192 -0
- equiframe/diff_contracts.py +523 -0
- equiframe/docs/adoption.md +329 -0
- equiframe/docs/api.md +534 -0
- equiframe/docs/arquitecto.md +143 -0
- equiframe/docs/choosing-api.md +84 -0
- equiframe/docs/cientifico.md +180 -0
- equiframe/docs/compatibility.md +254 -0
- equiframe/docs/contributing.md +171 -0
- equiframe/docs/dependencies.md +237 -0
- equiframe/docs/eda.md +166 -0
- equiframe/docs/extending.md +158 -0
- equiframe/docs/fast-ci.md +52 -0
- equiframe/docs/first-use.md +78 -0
- equiframe/docs/index.md +142 -0
- equiframe/docs/ingestion.md +145 -0
- equiframe/docs/inicio.md +129 -0
- equiframe/docs/installed-private.md +97 -0
- equiframe/docs/journeys.md +362 -0
- equiframe/docs/lectura-conversion.md +211 -0
- equiframe/docs/licensing.md +17 -0
- equiframe/docs/maintenance.md +233 -0
- equiframe/docs/migration-ir.md +333 -0
- equiframe/docs/option-audit.md +45 -0
- equiframe/docs/outcome-gates.md +134 -0
- equiframe/docs/performance.md +263 -0
- equiframe/docs/piloto-simulado.md +56 -0
- equiframe/docs/quickstart.md +156 -0
- equiframe/docs/r_review.md +212 -0
- equiframe/docs/recipes.md +248 -0
- equiframe/docs/releasing.md +183 -0
- equiframe/docs/security.md +218 -0
- equiframe/docs/semantics.md +274 -0
- equiframe/docs/sqlite-snapshots.md +262 -0
- equiframe/docs/universal-sources.md +49 -0
- equiframe/docs/universalidad-cinco-pasos.md +107 -0
- equiframe/docs/upgrading.md +538 -0
- equiframe/duckdb_capture.py +410 -0
- equiframe/eda/__init__.py +36 -0
- equiframe/eda/__main__.py +3 -0
- equiframe/eda/_backend.py +612 -0
- equiframe/eda/_inventory.py +49 -0
- equiframe/eda/_paths.py +49 -0
- equiframe/eda/_report.py +291 -0
- equiframe/eda/api.py +535 -0
- equiframe/eda/cli.py +102 -0
- equiframe/eda/models.py +239 -0
- equiframe/errors.py +83 -0
- equiframe/evidence.py +375 -0
- equiframe/examples/integrations/eda_generic.py +60 -0
- equiframe/examples/integrations/outcome_gates.py +94 -0
- equiframe/examples/integrations/sqlite_snapshot.py +246 -0
- equiframe/examples/integrations/sqlite_snapshot_demo.py +226 -0
- equiframe/examples/integrations/universal_first_use.py +296 -0
- equiframe/examples/journeys/README.md +31 -0
- equiframe/examples/journeys/__init__.py +1 -0
- equiframe/examples/journeys/a_pandas_refactor.py +88 -0
- equiframe/examples/journeys/b_native_migration.py +181 -0
- equiframe/examples/journeys/c_retail_etl.py +460 -0
- equiframe/examples/journeys/common.py +136 -0
- equiframe/examples/journeys/d_financial_reconciliation.py +96 -0
- equiframe/examples/journeys/e_telemetry.py +135 -0
- equiframe/examples/journeys/f_data_science_preparation.py +84 -0
- equiframe/examples/journeys/g_external_snapshots.py +197 -0
- equiframe/examples/journeys/h_portable_ci.py +129 -0
- equiframe/examples/journeys/requirements.lock +249 -0
- equiframe/examples/journeys/verify_journeys.py +117 -0
- equiframe/examples/load_convert_compare.py +85 -0
- equiframe/execution.py +355 -0
- equiframe/execution_types.py +177 -0
- equiframe/ingestion.py +744 -0
- equiframe/local.py +126 -0
- equiframe/migration/__init__.py +94 -0
- equiframe/migration/_physical.py +8 -0
- equiframe/migration/backends/_timestamp_transport.py +109 -0
- equiframe/migration/backends/duckdb.py +651 -0
- equiframe/migration/backends/duckdb_decimal.py +125 -0
- equiframe/migration/backends/duckdb_time.py +87 -0
- equiframe/migration/backends/pandas.py +544 -0
- equiframe/migration/backends/polars.py +915 -0
- equiframe/migration/backends/polars_decimal.py +231 -0
- equiframe/migration/io.py +198 -0
- equiframe/migration/ports.py +96 -0
- equiframe/migration/runner.py +545 -0
- equiframe/migration/runner_types.py +405 -0
- equiframe/migration/types.py +354 -0
- equiframe/migration/validation.py +619 -0
- equiframe/normalization.py +358 -0
- equiframe/pipeline.py +274 -0
- equiframe/pipeline_types.py +247 -0
- equiframe/ports.py +39 -0
- equiframe/preprocessing.py +110 -0
- equiframe/profile_types.py +343 -0
- equiframe/profiles.py +447 -0
- equiframe/py.typed +0 -0
- equiframe/r_review.py +706 -0
- equiframe/reduction.py +518 -0
- equiframe/registry.py +121 -0
- equiframe/report_rendering.py +366 -0
- equiframe/report_types.py +294 -0
- equiframe/reporting.py +26 -0
- equiframe/rule_types.py +178 -0
- equiframe/rules.py +819 -0
- equiframe/schemas/r-execution-binding-1.0.schema.json +46 -0
- equiframe/schemas/r-review-binding-1.0.schema.json +28 -0
- equiframe/schemas/r-review-options-1.0.schema.json +38 -0
- equiframe/schemas/r-review-result-1.0.schema.json +207 -0
- equiframe/schemas/reviewed-decision-1.0.schema.json +3394 -0
- equiframe/serialization.py +152 -0
- equiframe/snapshot.py +296 -0
- equiframe/sources.py +283 -0
- equiframe/tables.py +459 -0
- equiframe/types.py +71 -0
- equiframe/values.py +252 -0
- equiframe/verification_request.py +35 -0
- equiframe/verify.py +799 -0
- equiframe/verify_types.py +255 -0
- equiframe/workflow.py +510 -0
- equiframe/workflow_request.py +38 -0
- equiframe-0.12.4.dist-info/METADATA +268 -0
- equiframe-0.12.4.dist-info/RECORD +170 -0
- equiframe-0.12.4.dist-info/WHEEL +4 -0
- equiframe-0.12.4.dist-info/entry_points.txt +3 -0
- equiframe-0.12.4.dist-info/licenses/LICENSE +21 -0
- equiframe-0.12.4.dist-info/licenses/THIRD_PARTY_NOTICES.md +42 -0
- equiframe-0.12.4.dist-info/licenses/tools/cyclonedx/LICENSE +201 -0
- equiframe-0.12.4.dist-info/licenses/tools/cyclonedx/PROVENANCE.md +18 -0
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
"""Polars compiler using native expressions and independent native execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections.abc import Mapping, Sequence
|
|
7
|
+
from decimal import Decimal, localcontext
|
|
8
|
+
from hashlib import sha256
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from equiframe._decimal import exact_decimal_context
|
|
12
|
+
from equiframe._timezones import (
|
|
13
|
+
_timezone_data,
|
|
14
|
+
_timezone_resource_hash,
|
|
15
|
+
_TimezoneData,
|
|
16
|
+
_TimezoneRule,
|
|
17
|
+
)
|
|
18
|
+
from equiframe.contracts import (
|
|
19
|
+
BooleanType,
|
|
20
|
+
Contract,
|
|
21
|
+
DateType,
|
|
22
|
+
DecimalType,
|
|
23
|
+
FloatType,
|
|
24
|
+
IntegerType,
|
|
25
|
+
LogicalType,
|
|
26
|
+
StringType,
|
|
27
|
+
TimestampType,
|
|
28
|
+
)
|
|
29
|
+
from equiframe.errors import CapabilityError
|
|
30
|
+
from equiframe.registry import PolarsTransformation
|
|
31
|
+
|
|
32
|
+
from ..ports import CompiledPlan, compiler_check, require_supported
|
|
33
|
+
from ..types import (
|
|
34
|
+
Aggregate,
|
|
35
|
+
Arithmetic,
|
|
36
|
+
Boolean,
|
|
37
|
+
Cast,
|
|
38
|
+
Coalesce,
|
|
39
|
+
Column,
|
|
40
|
+
Compare,
|
|
41
|
+
CompilationCheck,
|
|
42
|
+
Concatenate,
|
|
43
|
+
Conditional,
|
|
44
|
+
DecimalRound,
|
|
45
|
+
Deduplicate,
|
|
46
|
+
EquiJoin,
|
|
47
|
+
Expression,
|
|
48
|
+
Filter,
|
|
49
|
+
IRColumn,
|
|
50
|
+
IsNaN,
|
|
51
|
+
IsNull,
|
|
52
|
+
LiteralExpression,
|
|
53
|
+
LocalizeTimestamp,
|
|
54
|
+
Not,
|
|
55
|
+
Project,
|
|
56
|
+
RelationalPlan,
|
|
57
|
+
Scan,
|
|
58
|
+
Window,
|
|
59
|
+
)
|
|
60
|
+
from ..validation import (
|
|
61
|
+
ValidatedIR,
|
|
62
|
+
compilation_identity,
|
|
63
|
+
decimal_limit,
|
|
64
|
+
expression_type,
|
|
65
|
+
integer_bounds,
|
|
66
|
+
iter_expressions,
|
|
67
|
+
literal_scalar,
|
|
68
|
+
)
|
|
69
|
+
from ._timestamp_transport import _TimestampCodec, _TimestampPolarsTransformation
|
|
70
|
+
|
|
71
|
+
if TYPE_CHECKING:
|
|
72
|
+
import polars as pl
|
|
73
|
+
|
|
74
|
+
from equiframe.execution import RunContext
|
|
75
|
+
|
|
76
|
+
_COMPILER_VERSION = "migration-native-ir1.0/polars/product-semantics-v14"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _dtype(dtype: LogicalType) -> Any:
|
|
80
|
+
import polars as pl
|
|
81
|
+
|
|
82
|
+
if isinstance(dtype, IntegerType):
|
|
83
|
+
return getattr(pl, ("Int" if dtype.signed else "UInt") + str(dtype.bits))
|
|
84
|
+
if isinstance(dtype, DecimalType):
|
|
85
|
+
return pl.Decimal(dtype.precision, dtype.scale)
|
|
86
|
+
if isinstance(dtype, FloatType):
|
|
87
|
+
return pl.Float32 if dtype.bits == 32 else pl.Float64
|
|
88
|
+
if isinstance(dtype, BooleanType):
|
|
89
|
+
return pl.Boolean
|
|
90
|
+
if isinstance(dtype, StringType):
|
|
91
|
+
return pl.String
|
|
92
|
+
if isinstance(dtype, DateType):
|
|
93
|
+
return pl.Date
|
|
94
|
+
if isinstance(dtype, TimestampType):
|
|
95
|
+
return pl.Int64
|
|
96
|
+
raise CapabilityError("unsupported Polars IR type")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _native_name(position: int) -> str:
|
|
100
|
+
return f"_equiframe_column_{position}"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _native_frame(frame: pl.DataFrame) -> pl.DataFrame:
|
|
104
|
+
"""Enter a collision-free namespace before any native name resolution."""
|
|
105
|
+
return frame.rename({name: _native_name(i) for i, name in enumerate(frame.columns)})
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _logical_frame(frame: pl.DataFrame, names: Sequence[str]) -> pl.DataFrame:
|
|
109
|
+
"""Restore the declared names only after native expression evaluation."""
|
|
110
|
+
return frame.rename(dict(zip(frame.columns, names, strict=True)))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _column(name: str, names: Sequence[str]) -> pl.Expr:
|
|
114
|
+
"""Resolve a literal logical name in an already renamed native frame."""
|
|
115
|
+
import polars as pl
|
|
116
|
+
|
|
117
|
+
# Polars 1.30 cannot compose different escaped-name regex selectors. Even
|
|
118
|
+
# nth(index) resolves back to a name which may expand '*' or '^...$'. All
|
|
119
|
+
# evaluated frames therefore use safe generated names, never user patterns.
|
|
120
|
+
return pl.col(_native_name(names.index(name)))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _native_civil_year(seconds: pl.Expr) -> pl.Expr:
|
|
124
|
+
import polars as pl
|
|
125
|
+
|
|
126
|
+
days = seconds // 86400 + 719468
|
|
127
|
+
era = days // 146097
|
|
128
|
+
day_of_era = days - era * 146097
|
|
129
|
+
year_of_era = (
|
|
130
|
+
day_of_era - day_of_era // 1460 + day_of_era // 36524 - day_of_era // 146096
|
|
131
|
+
) // 365
|
|
132
|
+
year = year_of_era + era * 400
|
|
133
|
+
day_of_year = day_of_era - (365 * year_of_era + year_of_era // 4 - year_of_era // 100)
|
|
134
|
+
month_prime = (5 * day_of_year + 2) // 153
|
|
135
|
+
month = month_prime + pl.when(month_prime < 10).then(3).otherwise(-9)
|
|
136
|
+
return year + (month <= 2).cast(pl.Int64)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _native_civil_days(year: pl.Expr, month: int, day: int | pl.Expr) -> pl.Expr:
|
|
140
|
+
adjusted = year - int(month <= 2)
|
|
141
|
+
era = adjusted // 400
|
|
142
|
+
yoe = adjusted - era * 400
|
|
143
|
+
mp = month + (-3 if month > 2 else 9)
|
|
144
|
+
doy = (153 * mp + 2) // 5 + day - 1
|
|
145
|
+
return era * 146097 + yoe * 365 + yoe // 4 - yoe // 100 + doy - 719468
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _native_leap_year(year: pl.Expr) -> pl.Expr:
|
|
149
|
+
return (year % 4 == 0) & ((year % 100 != 0) | (year % 400 == 0))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _native_rule_seconds(year: pl.Expr, rule: _TimezoneRule) -> pl.Expr:
|
|
153
|
+
import polars as pl
|
|
154
|
+
|
|
155
|
+
if rule.kind == "M":
|
|
156
|
+
month, week, weekday = rule.values
|
|
157
|
+
first = _native_civil_days(year, month, 1)
|
|
158
|
+
day = 1 + (weekday - (first + 4) % 7) % 7 + (week - 1) * 7
|
|
159
|
+
if week == 5:
|
|
160
|
+
length: int | pl.Expr = 29 if month == 2 else 30 if month in (4, 6, 9, 11) else 31
|
|
161
|
+
if month == 2:
|
|
162
|
+
length = 28 + _native_leap_year(year).cast(pl.Int64)
|
|
163
|
+
day = day - pl.when(day > length).then(7).otherwise(0)
|
|
164
|
+
days = _native_civil_days(year, month, day)
|
|
165
|
+
else:
|
|
166
|
+
number = rule.values[0]
|
|
167
|
+
extra = _native_leap_year(year).cast(pl.Int64) if rule.kind == "J" and number >= 60 else 0
|
|
168
|
+
days = _native_civil_days(year, 1, 1) + number - int(rule.kind == "J") + extra
|
|
169
|
+
wide = pl.Decimal(38, 0)
|
|
170
|
+
return days.cast(wide) * pl.lit(Decimal(86400), dtype=wide) + pl.lit(
|
|
171
|
+
Decimal(rule.seconds), dtype=wide
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _native_timezone_offset(
|
|
176
|
+
frame: pl.DataFrame, data: _TimezoneData, context: RunContext
|
|
177
|
+
) -> pl.DataFrame:
|
|
178
|
+
import polars as pl
|
|
179
|
+
|
|
180
|
+
context.check()
|
|
181
|
+
seconds = pl.col("_seconds")
|
|
182
|
+
if data.transitions:
|
|
183
|
+
boundaries = pl.lit(pl.Series("_boundaries", data.transitions, dtype=pl.Int64))
|
|
184
|
+
history = pl.lit(pl.Series("_offset_values", data.interval_offsets, dtype=pl.Int64)).gather(
|
|
185
|
+
boundaries.search_sorted(seconds, side="right")
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
history = pl.lit(data.interval_offsets[0], dtype=pl.Int64)
|
|
189
|
+
continuation = data.continuation
|
|
190
|
+
if continuation is None:
|
|
191
|
+
return frame.with_columns(history.alias("_actual_offset"))
|
|
192
|
+
if continuation.daylight is None:
|
|
193
|
+
tail = pl.lit(continuation.standard, dtype=pl.Int64)
|
|
194
|
+
else:
|
|
195
|
+
start_rule, end_rule = continuation.start, continuation.end
|
|
196
|
+
if start_rule is None or end_rule is None:
|
|
197
|
+
raise AssertionError("Invalid internal TZif continuation")
|
|
198
|
+
if data.transitions and not frame.select((seconds >= data.transitions[-1]).any()).item():
|
|
199
|
+
return frame.with_columns(history.alias("_actual_offset"))
|
|
200
|
+
frame = frame.with_columns(_native_civil_year(seconds).alias("_year"))
|
|
201
|
+
events = []
|
|
202
|
+
expressions = []
|
|
203
|
+
for index, shift in enumerate((-1, 0, 1)):
|
|
204
|
+
first = "_start" + str(index)
|
|
205
|
+
last = "_end" + str(index)
|
|
206
|
+
expressions.append(
|
|
207
|
+
(
|
|
208
|
+
_native_rule_seconds(pl.col("_year") + shift, start_rule)
|
|
209
|
+
- continuation.standard
|
|
210
|
+
).alias(first)
|
|
211
|
+
)
|
|
212
|
+
expressions.append(
|
|
213
|
+
(
|
|
214
|
+
_native_rule_seconds(pl.col("_year") + shift, end_rule) - continuation.daylight
|
|
215
|
+
).alias(last)
|
|
216
|
+
)
|
|
217
|
+
events.extend([(first, continuation.daylight), (last, continuation.standard)])
|
|
218
|
+
frame = frame.with_columns(expressions)
|
|
219
|
+
frame = frame.with_columns(
|
|
220
|
+
pl.max_horizontal(
|
|
221
|
+
[
|
|
222
|
+
pl.when(seconds >= pl.col(name)).then(pl.col(name)).otherwise(None)
|
|
223
|
+
for name, _ in events
|
|
224
|
+
]
|
|
225
|
+
).alias("_last_transition")
|
|
226
|
+
)
|
|
227
|
+
tail = pl.lit(continuation.standard, dtype=pl.Int64)
|
|
228
|
+
for name, offset in events:
|
|
229
|
+
tail = pl.when(pl.col("_last_transition") == pl.col(name)).then(offset).otherwise(tail)
|
|
230
|
+
actual_offset = (
|
|
231
|
+
pl.when(seconds >= data.transitions[-1]).then(tail).otherwise(history)
|
|
232
|
+
if data.transitions
|
|
233
|
+
else tail
|
|
234
|
+
)
|
|
235
|
+
return frame.with_columns(actual_offset.alias("_actual_offset"))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _native_localized_frame(
|
|
239
|
+
frame: pl.DataFrame,
|
|
240
|
+
argument: pl.Expr,
|
|
241
|
+
*,
|
|
242
|
+
data: _TimezoneData,
|
|
243
|
+
unit: str,
|
|
244
|
+
ambiguous: str,
|
|
245
|
+
context: RunContext,
|
|
246
|
+
) -> tuple[pl.Series, pl.Series, pl.Series, pl.Series]:
|
|
247
|
+
import polars as pl
|
|
248
|
+
|
|
249
|
+
context.check()
|
|
250
|
+
if not (unit in ("s", "ms", "us", "ns") and ambiguous in ("reject", "earlier", "later")):
|
|
251
|
+
raise CapabilityError("Invalid timestamp policy")
|
|
252
|
+
factor = {"s": 1, "ms": 1000, "us": 1000000, "ns": 1000000000}[unit]
|
|
253
|
+
wide = pl.Decimal(38, 0)
|
|
254
|
+
state = frame.with_columns(argument.cast(pl.Int64).alias("_wall")).select("_wall")
|
|
255
|
+
seconds_view = unit == "s"
|
|
256
|
+
if seconds_view:
|
|
257
|
+
state = state.with_columns(
|
|
258
|
+
(pl.col("_wall").cast(wide) * pl.lit(Decimal(1000), dtype=wide))
|
|
259
|
+
.cast(pl.Int64, strict=True)
|
|
260
|
+
.alias("_wall")
|
|
261
|
+
)
|
|
262
|
+
factor = 1000
|
|
263
|
+
state = state.with_columns(
|
|
264
|
+
pl.lit(0, dtype=pl.UInt16).alias("_count"), pl.lit(None, dtype=wide).alias("_chosen")
|
|
265
|
+
)
|
|
266
|
+
for offset in data.offsets:
|
|
267
|
+
context.check()
|
|
268
|
+
candidate = pl.col("_wall").cast(wide) - pl.lit(Decimal(offset * factor), dtype=wide)
|
|
269
|
+
work = state.with_columns(candidate.alias("_candidate"))
|
|
270
|
+
work = work.with_columns(
|
|
271
|
+
(pl.col("_wall") // factor - offset).fill_null(0).alias("_seconds")
|
|
272
|
+
)
|
|
273
|
+
work = _native_timezone_offset(work, data, context)
|
|
274
|
+
valid = (pl.col("_wall").is_not_null() & (pl.col("_actual_offset") == offset)).fill_null(
|
|
275
|
+
False
|
|
276
|
+
)
|
|
277
|
+
work = work.with_columns(valid.alias("_valid_candidate"))
|
|
278
|
+
work = work.with_columns(
|
|
279
|
+
pl.when(pl.col("_valid_candidate"))
|
|
280
|
+
.then(pl.col("_candidate"))
|
|
281
|
+
.otherwise(None)
|
|
282
|
+
.alias("_candidate_option")
|
|
283
|
+
)
|
|
284
|
+
previous = pl.col("_chosen")
|
|
285
|
+
option = pl.col("_candidate_option")
|
|
286
|
+
better = option > previous if ambiguous == "later" else option < previous
|
|
287
|
+
choice = pl.when(previous.is_null() | better).then(option).otherwise(previous)
|
|
288
|
+
state = work.select(
|
|
289
|
+
pl.col("_wall"),
|
|
290
|
+
(pl.col("_count") + pl.col("_valid_candidate").cast(pl.UInt16)).alias("_count"),
|
|
291
|
+
choice.alias("_chosen"),
|
|
292
|
+
)
|
|
293
|
+
fits = pl.col("_chosen").is_between(
|
|
294
|
+
pl.lit(Decimal(-(2**63)), dtype=wide), pl.lit(Decimal(2**63 - 1), dtype=wide)
|
|
295
|
+
)
|
|
296
|
+
state = state.with_columns(
|
|
297
|
+
(pl.col("_chosen").is_not_null() & ~fits).fill_null(False).alias("_output_range_failed")
|
|
298
|
+
)
|
|
299
|
+
state = state.with_columns(
|
|
300
|
+
pl.when(fits)
|
|
301
|
+
.then(pl.col("_chosen"))
|
|
302
|
+
.otherwise(None)
|
|
303
|
+
.cast(pl.Int64, strict=True)
|
|
304
|
+
.alias("_value")
|
|
305
|
+
)
|
|
306
|
+
if seconds_view:
|
|
307
|
+
state = state.with_columns((pl.col("_value") // 1000).alias("_value"))
|
|
308
|
+
return (
|
|
309
|
+
state["_value"],
|
|
310
|
+
state["_count"],
|
|
311
|
+
state["_output_range_failed"],
|
|
312
|
+
state["_wall"].is_not_null(),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _expression(
|
|
317
|
+
expression: Expression,
|
|
318
|
+
columns: tuple[IRColumn, ...],
|
|
319
|
+
prepared: Mapping[int, pl.Expr] | None = None,
|
|
320
|
+
) -> pl.Expr:
|
|
321
|
+
import polars as pl
|
|
322
|
+
|
|
323
|
+
if prepared is not None and id(expression) in prepared:
|
|
324
|
+
return prepared[id(expression)]
|
|
325
|
+
if isinstance(expression, Column):
|
|
326
|
+
return _column(expression.name, tuple(column.name for column in columns))
|
|
327
|
+
if isinstance(expression, LiteralExpression):
|
|
328
|
+
scalar = literal_scalar(expression)
|
|
329
|
+
return pl.lit(scalar, dtype=_dtype(expression.logical_type))
|
|
330
|
+
if isinstance(expression, Arithmetic):
|
|
331
|
+
operand = expression_type(expression.left, columns).logical_type
|
|
332
|
+
if (
|
|
333
|
+
isinstance(operand, IntegerType)
|
|
334
|
+
or expression.operation == "divide"
|
|
335
|
+
or (expression.operation == "multiply" and isinstance(operand, DecimalType))
|
|
336
|
+
):
|
|
337
|
+
raise CapabilityError("arithmetic requires its native eager checkpoint")
|
|
338
|
+
scale = operand.scale if isinstance(operand, DecimalType) else 0
|
|
339
|
+
left = _expression(expression.left, columns, prepared).cast(
|
|
340
|
+
pl.Decimal(38, scale), strict=True
|
|
341
|
+
)
|
|
342
|
+
right = _expression(expression.right, columns, prepared).cast(
|
|
343
|
+
pl.Decimal(38, scale), strict=True
|
|
344
|
+
)
|
|
345
|
+
result = {
|
|
346
|
+
"add": lambda: left + right,
|
|
347
|
+
"subtract": lambda: left - right,
|
|
348
|
+
"multiply": lambda: left * right,
|
|
349
|
+
}[expression.operation]()
|
|
350
|
+
return result.cast(_dtype(expression.logical_type), strict=True)
|
|
351
|
+
if isinstance(expression, DecimalRound):
|
|
352
|
+
raise CapabilityError("DecimalRound requires its native eager checkpoint")
|
|
353
|
+
if isinstance(expression, LocalizeTimestamp):
|
|
354
|
+
raise CapabilityError("timestamp localization requires its native eager checkpoint")
|
|
355
|
+
if isinstance(expression, Coalesce):
|
|
356
|
+
return pl.coalesce([_expression(item, columns, prepared) for item in expression.arguments])
|
|
357
|
+
if isinstance(expression, Conditional):
|
|
358
|
+
return (
|
|
359
|
+
pl.when(_expression(expression.predicate, columns, prepared))
|
|
360
|
+
.then(_expression(expression.then, columns, prepared))
|
|
361
|
+
.otherwise(_expression(expression.otherwise, columns, prepared))
|
|
362
|
+
)
|
|
363
|
+
if isinstance(expression, Compare):
|
|
364
|
+
left = _expression(expression.left, columns, prepared)
|
|
365
|
+
right = _expression(expression.right, columns, prepared)
|
|
366
|
+
operation = {"eq": "eq", "ne": "ne", "lt": "lt", "le": "le", "gt": "gt", "ge": "ge"}[
|
|
367
|
+
expression.operation
|
|
368
|
+
]
|
|
369
|
+
result = getattr(left, operation)(right)
|
|
370
|
+
if isinstance(expression_type(expression.left, columns).logical_type, FloatType):
|
|
371
|
+
nan = left.is_nan().fill_null(False) | right.is_nan().fill_null(False)
|
|
372
|
+
result = pl.when(nan).then(pl.lit(expression.operation == "ne")).otherwise(result)
|
|
373
|
+
return (
|
|
374
|
+
pl.when(left.is_null() | right.is_null())
|
|
375
|
+
.then(pl.lit(None, dtype=pl.Boolean))
|
|
376
|
+
.otherwise(result)
|
|
377
|
+
)
|
|
378
|
+
if isinstance(expression, Boolean):
|
|
379
|
+
values = [_expression(item, columns, prepared) for item in expression.arguments]
|
|
380
|
+
result = values[0]
|
|
381
|
+
for value in values[1:]:
|
|
382
|
+
result = result & value if expression.operation == "and" else result | value
|
|
383
|
+
return result
|
|
384
|
+
argument = _expression(expression.argument, columns, prepared)
|
|
385
|
+
if isinstance(expression, Not):
|
|
386
|
+
return ~argument
|
|
387
|
+
if isinstance(expression, IsNull):
|
|
388
|
+
return argument.is_null()
|
|
389
|
+
if isinstance(expression, IsNaN):
|
|
390
|
+
return argument.is_nan().fill_null(False)
|
|
391
|
+
raise CapabilityError("unknown Polars expression")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _check_localizations(
|
|
395
|
+
frame: pl.DataFrame,
|
|
396
|
+
expression: Expression,
|
|
397
|
+
columns: tuple[IRColumn, ...],
|
|
398
|
+
prepared: Mapping[int, pl.Expr],
|
|
399
|
+
) -> None:
|
|
400
|
+
"""Validate each fallible native localization before relational pruning."""
|
|
401
|
+
import polars as pl
|
|
402
|
+
|
|
403
|
+
for item in iter_expressions(expression):
|
|
404
|
+
if not isinstance(item, LocalizeTimestamp):
|
|
405
|
+
continue
|
|
406
|
+
dtype = expression_type(item.argument, columns).logical_type
|
|
407
|
+
if not (isinstance(dtype, TimestampType)):
|
|
408
|
+
raise AssertionError("Internal invariant check failed.")
|
|
409
|
+
before = _expression(item.argument, columns, prepared).cast(pl.Int64)
|
|
410
|
+
after = _expression(item, columns, prepared).cast(pl.Int64)
|
|
411
|
+
# Decimal(38,0) subtraction is native and wide enough for the full
|
|
412
|
+
# difference of two signed int64 values. An int64 subtraction here
|
|
413
|
+
# would wrap too and silently certify the broken timestamp kernel.
|
|
414
|
+
maximum_offset = 86400 * {"s": 1, "ms": 1000, "us": 1000000, "ns": 1000000000}[dtype.unit]
|
|
415
|
+
delta = (after.cast(pl.Decimal(38, 0)) - before.cast(pl.Decimal(38, 0))).abs()
|
|
416
|
+
bad = (before.is_not_null() & after.is_null()) | (
|
|
417
|
+
delta > pl.lit(Decimal(maximum_offset), dtype=pl.Decimal(38, 0))
|
|
418
|
+
)
|
|
419
|
+
if frame.select(bad.fill_null(False).any()).item():
|
|
420
|
+
raise CapabilityError("Polars timestamp localization exceeds faithful int64 range")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _prepare_arithmetic(
|
|
424
|
+
frame: pl.DataFrame,
|
|
425
|
+
expressions: tuple[Expression, ...],
|
|
426
|
+
columns: tuple[IRColumn, ...],
|
|
427
|
+
context: RunContext,
|
|
428
|
+
zones: Mapping[str, _TimezoneData],
|
|
429
|
+
) -> tuple[pl.DataFrame, dict[int, pl.Expr]]:
|
|
430
|
+
import polars as pl
|
|
431
|
+
|
|
432
|
+
from .polars_decimal import divide, multiply
|
|
433
|
+
|
|
434
|
+
prepared: dict[int, pl.Expr] = {}
|
|
435
|
+
for expression in expressions:
|
|
436
|
+
for item in reversed(tuple(iter_expressions(expression))):
|
|
437
|
+
if id(item) in prepared:
|
|
438
|
+
continue
|
|
439
|
+
if isinstance(item, LocalizeTimestamp):
|
|
440
|
+
declared = expression_type(item.argument, columns).logical_type
|
|
441
|
+
if not isinstance(declared, TimestampType):
|
|
442
|
+
raise CapabilityError("timestamp localization requires a timestamp argument")
|
|
443
|
+
values, counts, outside, present = _native_localized_frame(
|
|
444
|
+
frame,
|
|
445
|
+
_expression(item.argument, columns, prepared),
|
|
446
|
+
data=zones[item.timezone],
|
|
447
|
+
unit=declared.unit,
|
|
448
|
+
ambiguous=item.ambiguous,
|
|
449
|
+
context=context,
|
|
450
|
+
)
|
|
451
|
+
if ((counts == 0) & present).any():
|
|
452
|
+
raise pl.exceptions.ComputeError(
|
|
453
|
+
"Timestamp localization: nonexistent wall time"
|
|
454
|
+
)
|
|
455
|
+
if item.ambiguous == "reject" and ((counts > 1) & present).any():
|
|
456
|
+
raise pl.exceptions.ComputeError("Timestamp localization: ambiguous wall time")
|
|
457
|
+
if outside.any():
|
|
458
|
+
raise CapabilityError(
|
|
459
|
+
"Polars timestamp localization exceeds faithful int64 range"
|
|
460
|
+
)
|
|
461
|
+
prefix = f"_equiframe_timestamp_{len(prepared)}_"
|
|
462
|
+
while any(name.startswith(prefix) for name in frame.columns):
|
|
463
|
+
prefix += "_"
|
|
464
|
+
name = prefix + "result"
|
|
465
|
+
frame = frame.with_columns(values.alias(name))
|
|
466
|
+
prepared[id(item)] = pl.col(name)
|
|
467
|
+
context.check()
|
|
468
|
+
continue
|
|
469
|
+
if isinstance(item, DecimalRound):
|
|
470
|
+
declared = expression_type(item.argument, columns).logical_type
|
|
471
|
+
if not isinstance(declared, DecimalType):
|
|
472
|
+
raise CapabilityError("DecimalRound requires a Decimal argument")
|
|
473
|
+
prefix = f"_equiframe_round_{len(prepared)}_"
|
|
474
|
+
while any(name.startswith(prefix) for name in frame.columns):
|
|
475
|
+
prefix += "_"
|
|
476
|
+
# The checked native coefficient quotient rounds before its
|
|
477
|
+
# destination cast. A wide input carry never materializes a
|
|
478
|
+
# 39-digit value in the narrower input Decimal storage.
|
|
479
|
+
frame, result = divide(
|
|
480
|
+
frame,
|
|
481
|
+
_expression(item.argument, columns, prepared),
|
|
482
|
+
pl.lit(Decimal(1), dtype=pl.Decimal(1, 0)),
|
|
483
|
+
left_scale=declared.scale,
|
|
484
|
+
right_scale=0,
|
|
485
|
+
precision=item.logical_type.precision,
|
|
486
|
+
scale=item.logical_type.scale,
|
|
487
|
+
rounding=item.rounding,
|
|
488
|
+
prefix=prefix,
|
|
489
|
+
context=context,
|
|
490
|
+
)
|
|
491
|
+
prepared[id(item)] = result
|
|
492
|
+
continue
|
|
493
|
+
if not isinstance(item, Arithmetic):
|
|
494
|
+
continue
|
|
495
|
+
left = expression_type(item.left, columns).logical_type
|
|
496
|
+
right = expression_type(item.right, columns).logical_type
|
|
497
|
+
output = item.logical_type
|
|
498
|
+
if isinstance(left, IntegerType) and isinstance(output, IntegerType):
|
|
499
|
+
prefix = f"_equiframe_arithmetic_{len(prepared)}_"
|
|
500
|
+
lower, upper = integer_bounds(output)
|
|
501
|
+
first = _expression(item.left, columns, prepared)
|
|
502
|
+
second = _expression(item.right, columns, prepared)
|
|
503
|
+
context.check()
|
|
504
|
+
if item.operation == "multiply":
|
|
505
|
+
# UInt64 products can exceed signed Int128 and Decimal128.
|
|
506
|
+
# Prove a safe coefficient bound before any native product.
|
|
507
|
+
frame, result = multiply(
|
|
508
|
+
frame,
|
|
509
|
+
first,
|
|
510
|
+
second,
|
|
511
|
+
precision=len(str(max(abs(lower), abs(upper)))),
|
|
512
|
+
scale=0,
|
|
513
|
+
prefix=prefix,
|
|
514
|
+
context=context,
|
|
515
|
+
)
|
|
516
|
+
else:
|
|
517
|
+
first = first.cast(pl.String).cast(pl.Decimal(38, 0))
|
|
518
|
+
second = second.cast(pl.String).cast(pl.Decimal(38, 0))
|
|
519
|
+
raw = first + second if item.operation == "add" else first - second
|
|
520
|
+
frame = frame.with_columns(raw.alias(prefix + "result"))
|
|
521
|
+
result = pl.col(prefix + "result")
|
|
522
|
+
# Check the exact signed/unsigned destination range before its
|
|
523
|
+
# native integer cast; a cast must never conceal an overflow.
|
|
524
|
+
low = pl.lit(str(lower)).cast(pl.Decimal(38, 0))
|
|
525
|
+
high = pl.lit(str(upper)).cast(pl.Decimal(38, 0))
|
|
526
|
+
if frame.select(((result < low) | (result > high)).any()).item():
|
|
527
|
+
raise CapabilityError("Polars integer arithmetic exceeds declared range")
|
|
528
|
+
context.check()
|
|
529
|
+
prepared[id(item)] = result.cast(pl.String).cast(_dtype(output), strict=True)
|
|
530
|
+
continue
|
|
531
|
+
if item.operation not in ("divide", "multiply"):
|
|
532
|
+
continue
|
|
533
|
+
if not (
|
|
534
|
+
isinstance(left, DecimalType)
|
|
535
|
+
and isinstance(right, DecimalType)
|
|
536
|
+
and isinstance(output, DecimalType)
|
|
537
|
+
):
|
|
538
|
+
raise CapabilityError("Decimal arithmetic requires declared Decimal types")
|
|
539
|
+
if item.operation == "divide":
|
|
540
|
+
frame, result = divide(
|
|
541
|
+
frame,
|
|
542
|
+
_expression(item.left, columns, prepared),
|
|
543
|
+
_expression(item.right, columns, prepared),
|
|
544
|
+
left_scale=left.scale,
|
|
545
|
+
right_scale=right.scale,
|
|
546
|
+
precision=output.precision,
|
|
547
|
+
scale=output.scale,
|
|
548
|
+
rounding=item.rounding,
|
|
549
|
+
prefix=f"_equiframe_arithmetic_{len(prepared)}_",
|
|
550
|
+
context=context,
|
|
551
|
+
)
|
|
552
|
+
else:
|
|
553
|
+
frame, result = multiply(
|
|
554
|
+
frame,
|
|
555
|
+
_expression(item.left, columns, prepared),
|
|
556
|
+
_expression(item.right, columns, prepared),
|
|
557
|
+
precision=output.precision,
|
|
558
|
+
scale=output.scale,
|
|
559
|
+
prefix=f"_equiframe_arithmetic_{len(prepared)}_",
|
|
560
|
+
context=context,
|
|
561
|
+
)
|
|
562
|
+
prepared[id(item)] = result
|
|
563
|
+
return frame, prepared
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _coerce(frame: pl.DataFrame, validated: ValidatedIR, node_id: str) -> pl.DataFrame:
|
|
567
|
+
for column in validated.schema(node_id):
|
|
568
|
+
dtype, series = column.logical_type, frame[column.name]
|
|
569
|
+
if not column.nullable and series.null_count():
|
|
570
|
+
raise CapabilityError("IR output contains forbidden NULL: " + column.name)
|
|
571
|
+
if isinstance(dtype, (IntegerType, DecimalType)) and frame.height:
|
|
572
|
+
lower, upper = (
|
|
573
|
+
integer_bounds(dtype)
|
|
574
|
+
if isinstance(dtype, IntegerType)
|
|
575
|
+
else (decimal_limit(dtype).copy_negate(), decimal_limit(dtype))
|
|
576
|
+
)
|
|
577
|
+
minimum, maximum = series.min(), series.max()
|
|
578
|
+
if minimum is not None:
|
|
579
|
+
if not isinstance(minimum, (int, Decimal)) or not isinstance(
|
|
580
|
+
maximum, (int, Decimal)
|
|
581
|
+
):
|
|
582
|
+
raise CapabilityError("numeric IR column has incompatible native values")
|
|
583
|
+
if minimum < lower or maximum > upper:
|
|
584
|
+
raise CapabilityError(
|
|
585
|
+
"IR output exceeds declared numeric range: " + column.name
|
|
586
|
+
)
|
|
587
|
+
names = frame.columns
|
|
588
|
+
selected = _native_frame(frame).select(
|
|
589
|
+
[
|
|
590
|
+
_column(c.name, names).cast(_dtype(c.logical_type), strict=True)
|
|
591
|
+
for c in validated.schema(node_id)
|
|
592
|
+
]
|
|
593
|
+
)
|
|
594
|
+
return _logical_frame(selected, [c.name for c in validated.schema(node_id)])
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _aggregate(frame: pl.DataFrame, node: Aggregate, validated: ValidatedIR) -> pl.DataFrame:
|
|
598
|
+
import polars as pl
|
|
599
|
+
|
|
600
|
+
names = frame.columns
|
|
601
|
+
frame = _native_frame(frame)
|
|
602
|
+
if node.null_groups == "drop" and node.group_by:
|
|
603
|
+
frame = frame.filter(
|
|
604
|
+
pl.all_horizontal([_column(name, names).is_not_null() for name in node.group_by])
|
|
605
|
+
)
|
|
606
|
+
expressions = []
|
|
607
|
+
for i, measure in enumerate(node.measures):
|
|
608
|
+
if measure.operation == "count_rows":
|
|
609
|
+
result = pl.len()
|
|
610
|
+
else:
|
|
611
|
+
if not (measure.column is not None):
|
|
612
|
+
raise AssertionError("Internal invariant check failed.")
|
|
613
|
+
column = _column(measure.column, names)
|
|
614
|
+
if measure.operation == "count_valid":
|
|
615
|
+
result = column.count()
|
|
616
|
+
elif measure.operation == "sum":
|
|
617
|
+
scale = (
|
|
618
|
+
measure.logical_type.scale
|
|
619
|
+
if isinstance(measure.logical_type, DecimalType)
|
|
620
|
+
else 0
|
|
621
|
+
)
|
|
622
|
+
promoted = column.cast(pl.Decimal(38, scale), strict=True)
|
|
623
|
+
# Polars grouped Decimal SUM can wrap i128. A conservative native
|
|
624
|
+
# bound proves safety before any sum; it may reject cancellation.
|
|
625
|
+
if frame.height:
|
|
626
|
+
with localcontext(exact_decimal_context()):
|
|
627
|
+
threshold = Decimal((10**38 - 1) // frame.height).scaleb(-scale)
|
|
628
|
+
maximum = frame.select(promoted.abs().max()).item()
|
|
629
|
+
if maximum is not None and maximum > threshold:
|
|
630
|
+
raise CapabilityError(
|
|
631
|
+
"Polars SUM exceeds conservative exact accumulation bound"
|
|
632
|
+
)
|
|
633
|
+
zero = Decimal((0, (0,), -scale))
|
|
634
|
+
result = (
|
|
635
|
+
pl.when(column.count() == 0)
|
|
636
|
+
.then(
|
|
637
|
+
pl.lit(
|
|
638
|
+
None if node.empty_sum == "null" else zero, dtype=pl.Decimal(38, scale)
|
|
639
|
+
)
|
|
640
|
+
)
|
|
641
|
+
.otherwise(promoted.sum())
|
|
642
|
+
)
|
|
643
|
+
else:
|
|
644
|
+
result = getattr(column, measure.operation)()
|
|
645
|
+
expressions.append(result.alias(f"_equiframe_measure_{i}"))
|
|
646
|
+
aggregated = (
|
|
647
|
+
frame.group_by([_column(name, names) for name in node.group_by], maintain_order=True).agg(
|
|
648
|
+
expressions
|
|
649
|
+
)
|
|
650
|
+
if node.group_by
|
|
651
|
+
else frame.select(expressions)
|
|
652
|
+
)
|
|
653
|
+
logical = _logical_frame(aggregated, [*node.group_by, *(m.name for m in node.measures)])
|
|
654
|
+
return _coerce(logical, validated, node.node_id)
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _join(left: pl.DataFrame, right: pl.DataFrame, node: EquiJoin) -> pl.DataFrame:
|
|
658
|
+
"""Keep join-key selectors scalar using collision-free native-local names."""
|
|
659
|
+
# Polars forbids selector expressions as join keys, including by_name().
|
|
660
|
+
# Rename every column simultaneously into disjoint safe namespaces; this
|
|
661
|
+
# also handles inputs whose real names equal one of these internal names.
|
|
662
|
+
left_names = {name: f"_equiframe_join_left_{i}" for i, name in enumerate(left.columns)}
|
|
663
|
+
right_names = {name: f"_equiframe_join_right_{i}" for i, name in enumerate(right.columns)}
|
|
664
|
+
joined = left.rename(left_names).join(
|
|
665
|
+
right.rename(right_names),
|
|
666
|
+
left_on=[left_names[key.left] for key in node.keys],
|
|
667
|
+
right_on=[right_names[key.right] for key in node.keys],
|
|
668
|
+
how=node.how,
|
|
669
|
+
nulls_equal=node.null_keys == "equal",
|
|
670
|
+
coalesce=False,
|
|
671
|
+
maintain_order="left_right",
|
|
672
|
+
)
|
|
673
|
+
restored = {temporary: name for name, temporary in left_names.items()}
|
|
674
|
+
if node.how not in ("semi", "anti"):
|
|
675
|
+
restored.update(
|
|
676
|
+
{temporary: node.right_prefix + name for name, temporary in right_names.items()}
|
|
677
|
+
)
|
|
678
|
+
return joined.rename(restored)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _plan_timezone_data(plan: RelationalPlan) -> dict[str, _TimezoneData]:
|
|
682
|
+
zones: dict[str, _TimezoneData] = {}
|
|
683
|
+
for node in plan.nodes:
|
|
684
|
+
roots = (
|
|
685
|
+
tuple(item.expression for item in node.columns)
|
|
686
|
+
if isinstance(node, Project)
|
|
687
|
+
else (node.predicate,)
|
|
688
|
+
if isinstance(node, Filter)
|
|
689
|
+
else ()
|
|
690
|
+
)
|
|
691
|
+
for root in roots:
|
|
692
|
+
for expression in iter_expressions(root):
|
|
693
|
+
if isinstance(expression, LocalizeTimestamp) and expression.timezone not in zones:
|
|
694
|
+
zones[expression.timezone] = _timezone_data(expression.timezone)
|
|
695
|
+
return zones
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
class PolarsCompiler:
|
|
699
|
+
engine = "polars"
|
|
700
|
+
|
|
701
|
+
def check(self, plan: RelationalPlan, contract: Contract) -> CompilationCheck:
|
|
702
|
+
return compiler_check(self.engine, plan, contract)
|
|
703
|
+
|
|
704
|
+
def compile(self, plan: RelationalPlan, contract: Contract) -> CompiledPlan:
|
|
705
|
+
check = self.check(plan, contract)
|
|
706
|
+
if not check.supported:
|
|
707
|
+
raise CapabilityError(check.diagnostics[0].message)
|
|
708
|
+
validated = require_supported(self.engine, plan, contract)
|
|
709
|
+
zones = _plan_timezone_data(plan)
|
|
710
|
+
version = _COMPILER_VERSION
|
|
711
|
+
if zones:
|
|
712
|
+
resource_bytes = json.dumps(
|
|
713
|
+
{key: value.resource_sha256 for key, value in sorted(zones.items())},
|
|
714
|
+
sort_keys=True,
|
|
715
|
+
separators=(",", ":"),
|
|
716
|
+
).encode()
|
|
717
|
+
version += "/tzif-" + sha256(resource_bytes).hexdigest()
|
|
718
|
+
identifier, ir_sha, contract_sha = compilation_identity(
|
|
719
|
+
validated,
|
|
720
|
+
self.engine,
|
|
721
|
+
implementation_version=version,
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
def transform(tables: Mapping[str, pl.DataFrame], context: RunContext) -> pl.DataFrame:
|
|
725
|
+
import polars as pl
|
|
726
|
+
|
|
727
|
+
for key, data in zones.items():
|
|
728
|
+
context.check()
|
|
729
|
+
if _timezone_resource_hash(key) != data.resource_sha256:
|
|
730
|
+
raise CapabilityError("timezone resource changed after compilation")
|
|
731
|
+
frames: dict[str, pl.DataFrame] = {}
|
|
732
|
+
for node in plan.nodes:
|
|
733
|
+
context.check()
|
|
734
|
+
if isinstance(node, Scan):
|
|
735
|
+
frame = tables[node.table].clone()
|
|
736
|
+
elif isinstance(node, Project):
|
|
737
|
+
native, prepared = _prepare_arithmetic(
|
|
738
|
+
_native_frame(frames[node.source]),
|
|
739
|
+
tuple(item.expression for item in node.columns),
|
|
740
|
+
validated.schema(node.source),
|
|
741
|
+
context,
|
|
742
|
+
zones,
|
|
743
|
+
)
|
|
744
|
+
for item in node.columns:
|
|
745
|
+
_check_localizations(
|
|
746
|
+
native, item.expression, validated.schema(node.source), prepared
|
|
747
|
+
)
|
|
748
|
+
# Stored source frames have validated order after _coerce.
|
|
749
|
+
# Evaluate every expression against the same renamed source;
|
|
750
|
+
# distinct temporary output names preserve simultaneous aliases.
|
|
751
|
+
frame = native.with_columns(
|
|
752
|
+
[
|
|
753
|
+
_expression(
|
|
754
|
+
item.expression, validated.schema(node.source), prepared
|
|
755
|
+
).alias(f"_equiframe_project_{i}")
|
|
756
|
+
for i, item in enumerate(node.columns)
|
|
757
|
+
]
|
|
758
|
+
)
|
|
759
|
+
frame = frame.select(
|
|
760
|
+
[pl.col(f"_equiframe_project_{i}") for i in range(len(node.columns))]
|
|
761
|
+
)
|
|
762
|
+
frame = _logical_frame(frame, [item.name for item in node.columns])
|
|
763
|
+
elif isinstance(node, Filter):
|
|
764
|
+
source = frames[node.source]
|
|
765
|
+
native, prepared = _prepare_arithmetic(
|
|
766
|
+
_native_frame(source),
|
|
767
|
+
(node.predicate,),
|
|
768
|
+
validated.schema(node.source),
|
|
769
|
+
context,
|
|
770
|
+
zones,
|
|
771
|
+
)
|
|
772
|
+
_check_localizations(
|
|
773
|
+
native, node.predicate, validated.schema(node.source), prepared
|
|
774
|
+
)
|
|
775
|
+
frame = _logical_frame(
|
|
776
|
+
native.filter(
|
|
777
|
+
_expression(node.predicate, validated.schema(node.source), prepared)
|
|
778
|
+
).select([pl.col(_native_name(i)) for i in range(len(source.columns))]),
|
|
779
|
+
source.columns,
|
|
780
|
+
)
|
|
781
|
+
elif isinstance(node, Cast):
|
|
782
|
+
frame = frames[node.source]
|
|
783
|
+
elif isinstance(node, EquiJoin):
|
|
784
|
+
for side, names, unique in (
|
|
785
|
+
(
|
|
786
|
+
frames[node.left],
|
|
787
|
+
[key.left for key in node.keys],
|
|
788
|
+
node.cardinality in ("one_to_one", "one_to_many"),
|
|
789
|
+
),
|
|
790
|
+
(
|
|
791
|
+
frames[node.right],
|
|
792
|
+
[key.right for key in node.keys],
|
|
793
|
+
node.cardinality in ("one_to_one", "many_to_one"),
|
|
794
|
+
),
|
|
795
|
+
):
|
|
796
|
+
native = _native_frame(side)
|
|
797
|
+
matchable = (
|
|
798
|
+
native
|
|
799
|
+
if node.null_keys == "equal"
|
|
800
|
+
else native.filter(
|
|
801
|
+
pl.all_horizontal(
|
|
802
|
+
[_column(name, side.columns).is_not_null() for name in names]
|
|
803
|
+
)
|
|
804
|
+
)
|
|
805
|
+
)
|
|
806
|
+
if (
|
|
807
|
+
unique
|
|
808
|
+
and matchable.select(
|
|
809
|
+
pl.struct([_column(name, side.columns) for name in names])
|
|
810
|
+
.is_duplicated()
|
|
811
|
+
.any()
|
|
812
|
+
).item()
|
|
813
|
+
):
|
|
814
|
+
raise CapabilityError(
|
|
815
|
+
"Join violates declared cardinality: " + node.cardinality
|
|
816
|
+
)
|
|
817
|
+
frame = _join(frames[node.left], frames[node.right], node)
|
|
818
|
+
elif isinstance(node, Window):
|
|
819
|
+
names = frames[node.source].columns
|
|
820
|
+
frame = _native_frame(frames[node.source])
|
|
821
|
+
tie_keys = list(
|
|
822
|
+
dict.fromkeys((*node.partition_by, *(key.column for key in node.by)))
|
|
823
|
+
)
|
|
824
|
+
if frame.select(
|
|
825
|
+
pl.struct([_column(name, names) for name in tie_keys]).is_duplicated().any()
|
|
826
|
+
).item():
|
|
827
|
+
raise CapabilityError("Window encountered forbidden order ties")
|
|
828
|
+
frame = frame.sort(
|
|
829
|
+
[_column(key.column, names) for key in node.by],
|
|
830
|
+
descending=[key.direction == "descending" for key in node.by],
|
|
831
|
+
nulls_last=[key.nulls == "last" for key in node.by],
|
|
832
|
+
maintain_order=True,
|
|
833
|
+
)
|
|
834
|
+
window_expressions: list[pl.Expr] = []
|
|
835
|
+
for window_measure in node.measures:
|
|
836
|
+
if window_measure.operation == "row_number":
|
|
837
|
+
value = pl.int_range(1, pl.len() + 1, dtype=pl.Int64)
|
|
838
|
+
else:
|
|
839
|
+
if window_measure.column is None:
|
|
840
|
+
raise CapabilityError("lag/lead require an input column")
|
|
841
|
+
offset = (
|
|
842
|
+
window_measure.offset
|
|
843
|
+
if window_measure.operation == "lag"
|
|
844
|
+
else -window_measure.offset
|
|
845
|
+
)
|
|
846
|
+
value = _column(window_measure.column, names).shift(offset)
|
|
847
|
+
if node.partition_by:
|
|
848
|
+
value = value.over([_column(name, names) for name in node.partition_by])
|
|
849
|
+
window_expressions.append(
|
|
850
|
+
value.alias(f"_equiframe_window_{len(window_expressions)}")
|
|
851
|
+
)
|
|
852
|
+
frame = frame.with_columns(window_expressions)
|
|
853
|
+
frame = _logical_frame(
|
|
854
|
+
frame, [*names, *(window_measure.name for window_measure in node.measures)]
|
|
855
|
+
)
|
|
856
|
+
elif isinstance(node, Aggregate):
|
|
857
|
+
frame = _aggregate(frames[node.source], node, validated)
|
|
858
|
+
elif isinstance(node, Concatenate):
|
|
859
|
+
frame = pl.concat([frames[ref] for ref in node.sources], how="vertical")
|
|
860
|
+
elif isinstance(node, Deduplicate):
|
|
861
|
+
names = frames[node.source].columns
|
|
862
|
+
frame = _native_frame(frames[node.source])
|
|
863
|
+
if node.keep == "first":
|
|
864
|
+
tie_keys = list(
|
|
865
|
+
dict.fromkeys((*node.keys, *(key.column for key in node.by)))
|
|
866
|
+
)
|
|
867
|
+
if frame.select(
|
|
868
|
+
pl.struct([_column(name, names) for name in tie_keys])
|
|
869
|
+
.is_duplicated()
|
|
870
|
+
.any()
|
|
871
|
+
).item():
|
|
872
|
+
raise CapabilityError("Deduplicate encountered forbidden tiebreak ties")
|
|
873
|
+
frame = frame.sort(
|
|
874
|
+
[_column(key.column, names) for key in node.by],
|
|
875
|
+
descending=[key.direction == "descending" for key in node.by],
|
|
876
|
+
nulls_last=[key.nulls == "last" for key in node.by],
|
|
877
|
+
maintain_order=True,
|
|
878
|
+
)
|
|
879
|
+
frame = frame.unique(
|
|
880
|
+
subset=[_native_name(names.index(name)) for name in node.keys],
|
|
881
|
+
keep="first",
|
|
882
|
+
maintain_order=True,
|
|
883
|
+
)
|
|
884
|
+
frame = _logical_frame(frame, names)
|
|
885
|
+
else:
|
|
886
|
+
names = frames[node.source].columns
|
|
887
|
+
frame = _native_frame(frames[node.source])
|
|
888
|
+
if (
|
|
889
|
+
node.ties == "reject"
|
|
890
|
+
and frame.select(
|
|
891
|
+
pl.struct([_column(key.column, names) for key in node.by])
|
|
892
|
+
.is_duplicated()
|
|
893
|
+
.any()
|
|
894
|
+
).item()
|
|
895
|
+
):
|
|
896
|
+
raise CapabilityError("Sort encountered forbidden ties")
|
|
897
|
+
frame = frame.sort(
|
|
898
|
+
[_column(key.column, names) for key in node.by],
|
|
899
|
+
descending=[key.direction == "descending" for key in node.by],
|
|
900
|
+
nulls_last=[key.nulls == "last" for key in node.by],
|
|
901
|
+
maintain_order=True,
|
|
902
|
+
)
|
|
903
|
+
frame = _logical_frame(frame, names)
|
|
904
|
+
frames[node.node_id] = _coerce(frame, validated, node.node_id)
|
|
905
|
+
return frames[plan.output]
|
|
906
|
+
|
|
907
|
+
codec = _TimestampCodec.for_contract(contract)
|
|
908
|
+
transformation = (
|
|
909
|
+
_TimestampPolarsTransformation(identifier, transform, version, timestamp_codec=codec)
|
|
910
|
+
if codec.inputs or codec.output
|
|
911
|
+
else PolarsTransformation(identifier, transform, version)
|
|
912
|
+
)
|
|
913
|
+
return CompiledPlan(
|
|
914
|
+
self.engine, transformation, ir_sha, contract_sha, self.check(plan, contract)
|
|
915
|
+
)
|