2525from orso .types import PYTHON_TO_ORSO_MAP
2626from orso .types import OrsoTypes
2727
28+ from opteryx .compiled .structures .relation_statistics import RelationStatistics
2829from opteryx .config import OPTERYX_DEBUG
2930from opteryx .connectors .base .base_connector import DEFAULT_MORSEL_SIZE
3031from opteryx .connectors .base .base_connector import INITIAL_CHUNK_SIZE
3132from opteryx .connectors .base .base_connector import MIN_CHUNK_SIZE
3233from opteryx .connectors .base .base_connector import BaseConnector
3334from opteryx .connectors .capabilities import LimitPushable
3435from opteryx .connectors .capabilities import PredicatePushable
36+ from opteryx .connectors .capabilities import Statistics
3537from opteryx .exceptions import DatasetReadError
3638from opteryx .exceptions import MissingDependencyError
3739from opteryx .exceptions import UnmetRequirementError
@@ -53,7 +55,7 @@ def _handle_operand(operand: Node, parameters: dict) -> Tuple[Any, dict]:
5355 return f":{ name } " , parameters
5456
5557
56- class SqlConnector (BaseConnector , LimitPushable , PredicatePushable ):
58+ class SqlConnector (BaseConnector , LimitPushable , PredicatePushable , Statistics ):
5759 __mode__ = "Sql"
5860 __type__ = "SQL"
5961
@@ -91,6 +93,7 @@ def __init__(self, *args, connection: str = None, engine=None, **kwargs):
9193 BaseConnector .__init__ (self , ** kwargs )
9294 LimitPushable .__init__ (self , ** kwargs )
9395 PredicatePushable .__init__ (self , ** kwargs )
96+ Statistics .__init__ (self , ** kwargs )
9497
9598 try :
9699 from sqlalchemy import MetaData
@@ -224,6 +227,67 @@ def read_dataset( # type:ignore
224227
225228 # DEBUG: print(f"time spent converting: {convert_time/1e9}s")
226229
230+ def collect_relation_stats (self ) -> RelationStatistics :
231+ from sqlalchemy import inspect
232+ from sqlalchemy .sql import text
233+
234+ stats = RelationStatistics ()
235+ dialect = self ._engine .dialect .name .lower ()
236+
237+ if dialect == "postgresql" :
238+ row_est = self ._engine .execute (
239+ text ("SELECT reltuples::BIGINT FROM pg_class WHERE relname = :t" ),
240+ {"t" : self .dataset },
241+ ).scalar ()
242+ stats .record_count_estimate = int (row_est )
243+
244+ pg_stats = self ._engine .execute (
245+ text ("""
246+ SELECT attname, n_distinct, null_frac, histogram_bounds
247+ FROM pg_stats
248+ WHERE tablename = :t
249+ """ ),
250+ {"t" : self .dataset },
251+ ).fetchall ()
252+
253+ for row in pg_stats :
254+ col = row ["attname" ]
255+ stats .cardinality_estimate [col ] = (
256+ int (row ["n_distinct" ]) if row ["n_distinct" ] > 0 else 0
257+ )
258+ stats .null_count [col ] = int (row ["null_frac" ] * row_est )
259+ bounds = row ["histogram_bounds" ]
260+ if bounds and isinstance (bounds , list ) and len (bounds ) >= 2 :
261+ stats .lower_bounds [col ] = bounds [0 ]
262+ stats .upper_bounds [col ] = bounds [- 1 ]
263+
264+ elif dialect in {"duckdb" , "sqlite" , "mysql" }:
265+ # fallback: query full stats for small/embedded engines
266+ columns = inspect (self ._engine ).get_columns (self .dataset )
267+ numeric_cols = [
268+ col ["name" ]
269+ for col in columns
270+ if str (col ["type" ]).lower ()
271+ in {"integer" , "bigint" , "float" , "real" , "numeric" , "double" }
272+ ]
273+
274+ # Build dynamic query
275+ parts = ["COUNT(*) AS count" ]
276+ for col in numeric_cols :
277+ parts .extend ([f"MIN({ col } ) AS min_{ col } " , f"MAX({ col } ) AS max_{ col } " ])
278+ q = f"SELECT { ', ' .join (parts )} FROM { self .dataset } "
279+ with self ._engine .connect () as conn :
280+ # DEBUG: print("READ STATS\n", str(q))
281+ result = conn .execute (text (q )).fetchone ()._asdict ()
282+
283+ stats .record_count = result ["count" ]
284+ stats .record_count_estimate = result ["count" ]
285+ for col in numeric_cols :
286+ stats .lower_bounds [col ] = result [f"min_{ col } " ]
287+ stats .upper_bounds [col ] = result [f"max_{ col } " ]
288+
289+ return stats
290+
227291 def get_dataset_schema (self ) -> RelationSchema :
228292 from sqlalchemy import Table
229293
@@ -289,4 +353,6 @@ def get_dataset_schema(self) -> RelationSchema:
289353 except Exception as err :
290354 raise DatasetReadError (f"Unable to read dataset '{ self .dataset } '." ) from err
291355
356+ self .schema .relation_statistics = self .collect_relation_stats ()
357+
292358 return self .schema
0 commit comments