@@ -430,22 +430,142 @@ class PreProcessedResultSet(ResultSet):
430430 """Result set for pre-formatted rows that don't need projection processing.
431431
432432 Used when rows are already projected and formatted (e.g., from SQLite intermediate storage).
433- Skips the document projection step and goes directly to formatting as tuples .
433+ Skips the document projection step but applies type conversions for projection function results .
434434 """
435435
436436 def _process_and_cache_batch (self , batch : List [Dict [str , Any ]]) -> None :
437437 """Process and cache a batch of pre-processed documents.
438438
439- Unlike the base ResultSet, this skips projection processing and directly formats results.
439+ Unlike the base ResultSet, this skips projection processing but still applies type conversions
440+ for values that came from projection functions (e.g., converting strings back to datetime objects).
440441 """
441442 if not batch :
442443 return
443- # Skip projection processing - rows are already in final form
444- # Just format to output format (tuple)
445- formatted_batch = [self ._format_result (doc ) for doc in batch ]
444+ # Skip full projection processing - rows are already in final form
445+ # But apply type conversions for projection function results
446+ converted_batch = [self ._convert_projection_types (doc ) for doc in batch ]
447+ # Format to output format (tuple)
448+ formatted_batch = [self ._format_result (doc ) for doc in converted_batch ]
446449 self ._cached_results .extend (formatted_batch )
447450 self ._total_fetched += len (batch )
448451
452+ def _convert_projection_types (self , doc : Dict [str , Any ]) -> Dict [str , Any ]:
453+ """Convert string values from SQLite back to proper types based on projection functions.
454+
455+ When projection functions produce datetime/date objects, SQLite stores them as strings.
456+ This method converts them back to proper Python types.
457+ """
458+ if not self ._execution_plan :
459+ return doc
460+
461+ from datetime import datetime , timezone
462+
463+ from .sql .projection_functions import ProjectionFunctionRegistry
464+
465+ converted = dict (doc )
466+
467+ # Get projection information from execution plan
468+ projection_functions = getattr (self ._execution_plan , "projection_functions" , {})
469+ column_aliases = getattr (self ._execution_plan , "column_aliases" , {})
470+
471+ if not projection_functions :
472+ return converted
473+
474+ _ = ProjectionFunctionRegistry ()
475+
476+ # Iterate through projection functions to convert values
477+ for field_name , func_info in projection_functions .items ():
478+ # The column name might be aliased, so check both the original name and the alias
479+ col_names_to_check = [field_name ]
480+ if field_name in column_aliases :
481+ col_names_to_check .append (column_aliases [field_name ])
482+ # Also check for the mongo_to_bracket_key format
483+ col_names_to_check .append (self ._mongo_to_bracket_key (field_name ))
484+
485+ col_name = None
486+ for check_name in col_names_to_check :
487+ if check_name in converted :
488+ col_name = check_name
489+ break
490+
491+ if not col_name :
492+ continue
493+
494+ value = converted [col_name ]
495+ if value is None :
496+ continue
497+
498+ # Extract function name and format parameter
499+ func_name = None
500+ format_param = None
501+
502+ if isinstance (func_info , dict ):
503+ func_name = func_info .get ("name" )
504+ format_param = func_info .get ("format_param" )
505+ elif isinstance (func_info , (list , tuple )):
506+ if len (func_info ) >= 1 :
507+ func_name = func_info [0 ]
508+ if len (func_info ) >= 2 :
509+ format_param = func_info [1 ]
510+
511+ if not func_name :
512+ continue
513+
514+ # Handle special cases for SQLite-stored projection function results
515+ if func_name .upper () in ("DATE" , "DATETIME" , "TIMESTAMP" ):
516+ # These functions should produce datetime objects
517+ try :
518+ if isinstance (value , str ):
519+ # Handle BSON Timestamp string representation: "Timestamp(timestamp_int, increment)"
520+ if value .startswith ("Timestamp(" ):
521+ match = re .match (r"Timestamp\((\d+),\s*\d+\)" , value )
522+ if match :
523+ timestamp_int = int (match .group (1 ))
524+ # Convert UNIX timestamp to datetime
525+ converted [col_name ] = datetime .fromtimestamp (timestamp_int , tz = timezone .utc ).replace (
526+ tzinfo = None
527+ )
528+ continue
529+
530+ # Try to parse as ISO format date/datetime string
531+ try :
532+ # Try full datetime first
533+ converted [col_name ] = datetime .fromisoformat (value )
534+ except (ValueError , TypeError ):
535+ try :
536+ # Try date only (YYYY-MM-DD)
537+ converted [col_name ] = datetime .strptime (value , "%Y-%m-%d" )
538+ except (ValueError , TypeError ):
539+ try :
540+ # Try with custom format if provided
541+ if format_param :
542+ converted [col_name ] = datetime .strptime (value , format_param )
543+ except (ValueError , TypeError ):
544+ # Keep original value if all conversions fail
545+ pass
546+ except Exception as e :
547+ _logger .debug (f"Error converting { col_name } using { func_name } : { e } " )
548+ # Keep original value if conversion fails
549+ elif func_name .upper () in ("NUMBER" , "INT" ):
550+ # These functions should produce numeric values
551+ try :
552+ if isinstance (value , str ):
553+ # Try to convert to float
554+ converted [col_name ] = float (value )
555+ except (ValueError , TypeError ):
556+ # Keep original value if conversion fails
557+ pass
558+ elif func_name .upper () == "BOOL" :
559+ # BOOL function should produce boolean values
560+ try :
561+ if isinstance (value , str ):
562+ converted [col_name ] = value .lower () in ("true" , "1" , "yes" )
563+ except Exception :
564+ # Keep original value if conversion fails
565+ pass
566+
567+ return converted
568+
449569
450570# For backward compatibility
451571MongoResultSet = ResultSet
0 commit comments