@@ -684,7 +684,7 @@ def _render_operations(self, operations, start_num=1, verbose=False):
684684 lines .append (f" └─ { k } : { v } " )
685685 return lines
686686
687- def explain (self , verbose : bool = False ) -> str :
687+ def explain (self , verbose : bool = False ) -> None :
688688 """
689689 Generate and display the execution plan in original operation order.
690690
@@ -696,15 +696,12 @@ def explain(self, verbose: bool = False) -> str:
696696 Args:
697697 verbose: If True, show additional details like full SQL queries
698698
699- Returns:
700- String representation of the execution plan
701-
702699 Example:
703700 >>> ds = DataStore.from_file("data.csv")
704701 >>> ds = ds.select('name', 'age').filter(ds.age > 25)
705702 >>> ds['computed'] = ds['age'] * 2
706703 >>> ds = ds.filter(ds['age'] < 50) # Order matters!
707- >>> print( ds.explain() )
704+ >>> ds.explain()
708705 """
709706 from .query_planner import QueryPlanner
710707
@@ -867,7 +864,6 @@ def explain(self, verbose: bool = False) -> str:
867864
868865 output = "\n " .join (lines )
869866 print (output )
870- return output
871867
872868 def _is_sql_query (self ) -> bool :
873869 """Check if this DataStore represents a SQL query."""
@@ -1089,6 +1085,58 @@ def _add_lazy_op(self, op: LazyOp):
10891085 self ._lazy_ops .append (op )
10901086 self ._invalidate_cache ()
10911087
1088+ def _get_source_df_if_pristine (self ) -> "Optional[pd.DataFrame]" :
1089+ """
1090+ For pristine DataStores (no transformations applied), return the
1091+ underlying source DataFrame directly. Returns None otherwise.
1092+
1093+ "Pristine" means:
1094+ - Cache is valid (already executed, result available), OR
1095+ - Exactly one LazyDataFrameSource op with no further ops
1096+
1097+ This enables zero-cost metadata access (dtypes, columns, shape)
1098+ for freshly created or already-executed DataStores.
1099+ """
1100+ if self ._is_cache_valid ():
1101+ return self ._cached_result
1102+ from .lazy_ops import LazyDataFrameSource
1103+ if (len (self ._lazy_ops ) == 1
1104+ and isinstance (self ._lazy_ops [0 ], LazyDataFrameSource )):
1105+ return self ._lazy_ops [0 ]._df
1106+ return None
1107+
1108+ def _is_pristine_sql_source (self ) -> bool :
1109+ """Check if this is a raw SQL/file source with no operations."""
1110+ return (not self ._lazy_ops
1111+ and (self ._table_function is not None or self .table_name is not None ))
1112+
1113+ def _probe_dtypes_from_sql_source (self ) -> "pd.Series" :
1114+ """
1115+ Get pandas dtypes for a pristine SQL source via a LIMIT 0 query.
1116+
1117+ Executes SELECT * ... LIMIT 0 which returns an empty DataFrame with
1118+ correct column types, without transferring any actual data.
1119+ """
1120+ if self ._executor is None :
1121+ self .connect ()
1122+
1123+ if self ._table_function :
1124+ source = self ._table_function .to_sql ()
1125+ elif self .table_name :
1126+ source = format_identifier (self .table_name , self .quote_char )
1127+ else :
1128+ return self ._get_df ().dtypes
1129+
1130+ sql = f"SELECT * FROM { source } LIMIT 0"
1131+ if self ._format_settings :
1132+ parts = []
1133+ for k , v in self ._format_settings .items ():
1134+ parts .append (f"{ k } ='{ v } '" if isinstance (v , str ) else f"{ k } ={ v } " )
1135+ sql += " SETTINGS " + ", " .join (parts )
1136+
1137+ result = self ._executor .execute (sql )
1138+ return result .to_df ().dtypes
1139+
10921140 def _execute (self ):
10931141 """
10941142 Execute all lazy operations into a DataFrame.
@@ -3598,9 +3646,10 @@ def sample(
35983646 @property
35993647 def shape (self ):
36003648 """
3601- Return the shape (rows, columns) of the query result .
3649+ Return the shape (rows, columns).
36023650
3603- Works correctly with both SQL queries and executed DataFrames.
3651+ Optimized for pristine sources: uses source DataFrame directly or
3652+ SQL COUNT(*) + DESCRIBE, avoiding full data loading.
36043653
36053654 Returns:
36063655 Tuple of (rows, columns)
@@ -3609,20 +3658,23 @@ def shape(self):
36093658 >>> ds = DataStore.from_file("data.csv")
36103659 >>> rows, cols = ds.select("*").shape
36113660 """
3612- # Use _get_df if available (handles caching properly)
3661+ src = self ._get_source_df_if_pristine ()
3662+ if src is not None :
3663+ return src .shape
3664+ if self ._is_pristine_sql_source ():
3665+ return (self .count_rows (), len (self .columns ))
36133666 if hasattr (self , "_get_df" ):
3614- df = self ._get_df ()
3615- else :
3616- df = self .to_df ()
3617- return df .shape
3667+ return self ._get_df ().shape
3668+ return self .to_df ().shape
36183669
36193670 @property
36203671 def columns (self ):
36213672 """
3622- Return the column names of the query result .
3673+ Return the column names.
36233674
3624- Works correctly with both SQL queries and executed DataFrames.
3625- Raises DataStoreError if no table is bound (connection-level DataStore).
3675+ Optimized for pristine sources: reads from the source DataFrame
3676+ or uses DESCRIBE (no data loading). For complex pipelines,
3677+ falls back to full execution.
36263678
36273679 Returns:
36283680 pandas Index of column names
@@ -3632,12 +3684,16 @@ def columns(self):
36323684 >>> cols = ds.select("*").columns
36333685 """
36343686 self ._require_table_context ("columns" )
3635- # Use _get_df if available (handles caching properly)
3687+ src = self ._get_source_df_if_pristine ()
3688+ if src is not None :
3689+ return src .columns
3690+ if self ._is_pristine_sql_source ():
3691+ schema = self .schema ()
3692+ if schema :
3693+ return pd .Index (schema .keys ())
36363694 if hasattr (self , "_get_df" ):
3637- df = self ._get_df ()
3638- else :
3639- df = self .to_df ()
3640- return df .columns
3695+ return self ._get_df ().columns
3696+ return self .to_df ().columns
36413697
36423698 @columns .setter
36433699 def columns (self , new_columns ):
0 commit comments