11"""SQLProjection — projections over ``__THIS__``, fit once, serve row-at-a-time.
22
3- The fit half works : window aggregates over ``__THIS__`` are marginalized into
3+ The fit half: window aggregates over ``__THIS__`` are marginalized into
44materialized params tables plus a rewritten ``serving_sql`` (see
5- ``_marginalize``). Transformers and author UDFs serve as scalar calls in that
6- SQL — ``transform`` registers them on the connection and runs; there is no
7- post-processing. The serving half (``infer``/``infer_batch`` through Confit)
8- is a later loop and still raises ``NotImplementedError``.
5+ ``_marginalize``); transformers fit per group into ``PythonTransform`` UDFs.
6+ The serving half is two bindings of the same artifact: ``transform`` runs
7+ ``serving_sql`` through DuckDB with the UDFs registered (batch, the oracle),
8+ and ``infer``/``infer_batch`` run it through Confit's ``DuckDBInferFn`` with
9+ the same UDF objects passed as ``udfs=`` (row-at-a-time, bit-exact with the
10+ DuckDB path by Confit's contract).
911"""
1012
1113from __future__ import annotations
@@ -32,7 +34,28 @@ def _feature_matrix(table: pa.Table, cols: list[str]):
3234 return mat
3335
3436
35- _TODO = "SQLProjection serving is a later loop; {} has no implementation yet"
37+ def _model_from_arrow (schema : pa .Schema ) -> type [BaseModel ]:
38+ """The serving row model, derived from the training table's schema (real
39+ types, guaranteed — a declared ``this_model`` may carry ``object``
40+ fields). Unmappable types become opaque ``object`` fields: Confit
41+ accepts those unless the SQL references them."""
42+ import pydantic
43+
44+ fields : dict [str , Any ] = {}
45+ for f in schema :
46+ t = f .type
47+ if pa .types .is_floating (t ):
48+ p : type = float
49+ elif pa .types .is_integer (t ):
50+ p = int
51+ elif pa .types .is_boolean (t ):
52+ p = bool
53+ elif pa .types .is_string (t ) or pa .types .is_large_string (t ):
54+ p = str
55+ else :
56+ p = object
57+ fields [f .name ] = (p | None if p is not object else Any , None )
58+ return pydantic .create_model ("Row" , ** fields )
3659
3760
3861class SQLProjection :
@@ -90,6 +113,8 @@ def _resolve(name: str):
90113 self ._marginalized = marginalize (sql , self ._columns , _resolve )
91114 self ._params : dict [str , pa .Table ] | None = None
92115 self ._udfs : dict [str , PythonTransform ] | None = None
116+ self ._row_model : type [BaseModel ] | None = None
117+ self ._fn : Any = None # confit.DuckDBInferFn, built lazily post-fit
93118
94119 @classmethod
95120 def from_file (cls , path : str ) -> SQLProjection :
@@ -147,6 +172,8 @@ def fit(self, table: pa.Table, /) -> SQLProjection:
147172 self ._params = {spec .name : materialized [spec .name ] for spec in m .params }
148173 finally :
149174 con .close ()
175+ self ._row_model = _model_from_arrow (table .schema )
176+ self ._fn = None # refit invalidates the prepared serving function
150177 return self
151178
152179 def _fit_step (self , step , table : pa .Table ) -> tuple [pa .Table , PythonTransform ]:
@@ -257,20 +284,47 @@ def udfs(self) -> dict[str, UDF]:
257284 out .update (self ._udfs )
258285 return out
259286
287+ def _serving_fn (self ):
288+ """The Confit binding of the fitted artifact, prepared once: the same
289+ ``serving_sql``, params tables, and UDF objects the DuckDB path uses —
290+ Confit's contract makes the two bit-exact or refuses by name."""
291+ if self ._params is None or self ._udfs is None or self ._row_model is None :
292+ raise MarginalizeError ("not fitted: call fit(table) first" )
293+ if self ._fn is None :
294+ from confit import DuckDBInferFn
295+
296+ m = self ._marginalized
297+ udfs = [self .transformers [n ] for n in m .scalar_udfs ]
298+ udfs += list (self ._udfs .values ())
299+ self ._fn = DuckDBInferFn (
300+ m .serving_sql ,
301+ row_tables = {"__THIS__" : self ._row_model },
302+ static_tables = dict (self ._params ),
303+ udfs = udfs ,
304+ shape = "map" ,
305+ )
306+ return self ._fn
307+
260308 @property
261309 def backend (self ) -> str :
262310 """Execution backend: "cranelift", "interpreter", or "constant"."""
263- raise NotImplementedError ( _TODO . format ( "backend" ))
311+ return self . _serving_fn (). backend
264312
265313 @property
266314 def boundary (self ) -> str :
267315 """Boundary path: "marshaller", "generic", or "constant"."""
268- raise NotImplementedError (_TODO .format ("boundary" ))
316+ return self ._serving_fn ().boundary
317+
318+ @property
319+ def output_model (self ) -> type [BaseModel ]:
320+ """The typed output row model ``infer`` returns instances of."""
321+ return self ._serving_fn ().output_model
269322
270323 def infer (self , row : dict [str , Any ] | BaseModel , / ) -> BaseModel :
271324 """Single-row inference; returns the typed output model instance."""
272- raise NotImplementedError (_TODO .format ("infer" ))
325+ (out ,) = self ._serving_fn ().infer_rows ([row ]) # shape="map": exactly one
326+ return out
273327
274328 def infer_batch (self , rows : list [dict [str , Any ] | BaseModel ], / ) -> list [BaseModel ]:
275329 """Many-rows inference; returns typed output model instances."""
276- raise NotImplementedError ( _TODO . format ( "infer_batch" ) )
330+ return self . _serving_fn (). infer_rows ( rows )
0 commit comments