66import datetime as dt
77import os
88import sys
9+ import threading
910import uuid
11+ from dataclasses import dataclass
1012from decimal import Decimal
1113from pathlib import Path
1214from typing import Any
@@ -97,6 +99,46 @@ class _LbugInt128(ctypes.Structure):
9799 _fields_ = [("low" , ctypes .c_uint64 ), ("high" , ctypes .c_int64 )]
98100
99101
102+ @dataclass (frozen = True )
103+ class CAPIJsonParameter :
104+ value : str
105+
106+
107+ class _ArrowSchema (ctypes .Structure ):
108+ pass
109+
110+
111+ _ArrowSchema ._fields_ = [
112+ ("format" , ctypes .c_char_p ),
113+ ("name" , ctypes .c_char_p ),
114+ ("metadata" , ctypes .c_char_p ),
115+ ("flags" , ctypes .c_int64 ),
116+ ("n_children" , ctypes .c_int64 ),
117+ ("children" , ctypes .POINTER (ctypes .POINTER (_ArrowSchema ))),
118+ ("dictionary" , ctypes .POINTER (_ArrowSchema )),
119+ ("release" , ctypes .c_void_p ),
120+ ("private_data" , ctypes .c_void_p ),
121+ ]
122+
123+
124+ class _ArrowArray (ctypes .Structure ):
125+ pass
126+
127+
128+ _ArrowArray ._fields_ = [
129+ ("length" , ctypes .c_int64 ),
130+ ("null_count" , ctypes .c_int64 ),
131+ ("offset" , ctypes .c_int64 ),
132+ ("n_buffers" , ctypes .c_int64 ),
133+ ("n_children" , ctypes .c_int64 ),
134+ ("buffers" , ctypes .POINTER (ctypes .c_void_p )),
135+ ("children" , ctypes .POINTER (ctypes .POINTER (_ArrowArray ))),
136+ ("dictionary" , ctypes .POINTER (_ArrowArray )),
137+ ("release" , ctypes .c_void_p ),
138+ ("private_data" , ctypes .c_void_p ),
139+ ]
140+
141+
100142def _resolve_library_path () -> str :
101143 override = os .getenv ("LBUG_C_API_LIB_PATH" )
102144 if override :
@@ -293,12 +335,20 @@ def _setup_signatures() -> None:
293335 _LIB .lbug_value_create_null .restype = ctypes .POINTER (_LbugValue )
294336 _LIB .lbug_value_create_bool .argtypes = [ctypes .c_bool ]
295337 _LIB .lbug_value_create_bool .restype = ctypes .POINTER (_LbugValue )
338+ _LIB .lbug_value_create_int8 .argtypes = [ctypes .c_int8 ]
339+ _LIB .lbug_value_create_int8 .restype = ctypes .POINTER (_LbugValue )
340+ _LIB .lbug_value_create_int16 .argtypes = [ctypes .c_int16 ]
341+ _LIB .lbug_value_create_int16 .restype = ctypes .POINTER (_LbugValue )
342+ _LIB .lbug_value_create_int32 .argtypes = [ctypes .c_int32 ]
343+ _LIB .lbug_value_create_int32 .restype = ctypes .POINTER (_LbugValue )
296344 _LIB .lbug_value_create_int64 .argtypes = [ctypes .c_int64 ]
297345 _LIB .lbug_value_create_int64 .restype = ctypes .POINTER (_LbugValue )
298346 _LIB .lbug_value_create_double .argtypes = [ctypes .c_double ]
299347 _LIB .lbug_value_create_double .restype = ctypes .POINTER (_LbugValue )
300348 _LIB .lbug_value_create_string .argtypes = [ctypes .c_char_p ]
301349 _LIB .lbug_value_create_string .restype = ctypes .POINTER (_LbugValue )
350+ _LIB .lbug_value_create_json .argtypes = [ctypes .c_char_p ]
351+ _LIB .lbug_value_create_json .restype = ctypes .POINTER (_LbugValue )
302352 _LIB .lbug_value_create_uuid .argtypes = [ctypes .c_char_p ]
303353 _LIB .lbug_value_create_uuid .restype = ctypes .POINTER (_LbugValue )
304354 _LIB .lbug_value_create_date .argtypes = [_LbugDate ]
@@ -371,6 +421,17 @@ def _setup_signatures() -> None:
371421 ]
372422 _LIB .lbug_query_result_get_next_query_result .restype = ctypes .c_int
373423 _LIB .lbug_query_result_reset_iterator .argtypes = [ctypes .POINTER (_LbugQueryResult )]
424+ _LIB .lbug_query_result_get_arrow_schema .argtypes = [
425+ ctypes .POINTER (_LbugQueryResult ),
426+ ctypes .POINTER (_ArrowSchema ),
427+ ]
428+ _LIB .lbug_query_result_get_arrow_schema .restype = ctypes .c_int
429+ _LIB .lbug_query_result_get_next_arrow_chunk .argtypes = [
430+ ctypes .POINTER (_LbugQueryResult ),
431+ ctypes .c_int64 ,
432+ ctypes .POINTER (_ArrowArray ),
433+ ]
434+ _LIB .lbug_query_result_get_next_arrow_chunk .restype = ctypes .c_int
374435 _LIB .lbug_query_result_get_query_summary .argtypes = [
375436 ctypes .POINTER (_LbugQueryResult ),
376437 ctypes .POINTER (_LbugQuerySummary ),
@@ -806,6 +867,11 @@ def _parse_rendered_value(value: str) -> Any:
806867 except (ValueError , SyntaxError ):
807868 return value
808869
870+ if candidate .lower () == "true" :
871+ return True
872+ if candidate .lower () == "false" :
873+ return False
874+
809875 # Parse plain numeric textual values.
810876 try :
811877 if "." in candidate or "e" in candidate .lower ():
@@ -818,9 +884,17 @@ def _parse_rendered_value(value: str) -> Any:
818884def _value_from_python (value : Any ) -> ctypes .POINTER (_LbugValue ):
819885 if value is None :
820886 return _LIB .lbug_value_create_null ()
887+ if isinstance (value , CAPIJsonParameter ):
888+ return _LIB .lbug_value_create_json (value .value .encode ())
821889 if isinstance (value , bool ):
822890 return _LIB .lbug_value_create_bool (value )
823891 if isinstance (value , int ) and not isinstance (value , bool ):
892+ if - (1 << 7 ) <= value <= (1 << 7 ) - 1 :
893+ return _LIB .lbug_value_create_int8 (value )
894+ if - (1 << 15 ) <= value <= (1 << 15 ) - 1 :
895+ return _LIB .lbug_value_create_int16 (value )
896+ if - (1 << 31 ) <= value <= (1 << 31 ) - 1 :
897+ return _LIB .lbug_value_create_int32 (value )
824898 return _LIB .lbug_value_create_int64 (value )
825899 if isinstance (value , float ):
826900 return _LIB .lbug_value_create_double (value )
@@ -1224,18 +1298,169 @@ def getExecutionTime(self) -> float:
12241298 finally :
12251299 _LIB .lbug_query_summary_destroy (ctypes .byref (summary ))
12261300
1227- def getAsArrow (self , * _args : Any , ** _kwargs : Any ) -> Any :
1228- raise NotImplementedError (
1229- "Arrow export is not yet implemented in C-API backend"
1301+ def getAsArrow (self , * args : Any , ** _kwargs : Any ) -> Any :
1302+ import pyarrow as pa
1303+
1304+ chunk_size = int (args [0 ]) if args else 0
1305+ fallback_extension_types = bool (args [1 ]) if len (args ) > 1 else False
1306+ num_tuples = int (self .getNumTuples ())
1307+ if chunk_size <= 0 :
1308+ chunk_size = max (num_tuples , 1 )
1309+
1310+ if "MAP" in self .getColumnDataTypes ():
1311+ rows = self ._get_all_rows_from_start ()
1312+ for row in rows :
1313+ for value in row :
1314+ if isinstance (value , dict ) and any (k is None for k in value ):
1315+ rendered = ", " .join (
1316+ f"{ '' if k is None else k } ={ v } " for k , v in value .items ()
1317+ )
1318+ msg = (
1319+ f"Cannot convert map with null key to Arrow: {{{ rendered } }}"
1320+ )
1321+ raise RuntimeError (msg )
1322+
1323+ schema_ptr = _ArrowSchema ()
1324+ _check_state (
1325+ _LIB .lbug_query_result_get_arrow_schema (
1326+ ctypes .byref (self ._result ), ctypes .byref (schema_ptr )
1327+ ),
1328+ "Failed to export Arrow schema" ,
12301329 )
1330+ schema = pa .Schema ._import_from_c (ctypes .addressof (schema_ptr ))
1331+
1332+ self .resetIterator ()
1333+ batches = []
1334+ try :
1335+ while self .hasNext ():
1336+ array_ptr = _ArrowArray ()
1337+ _check_state (
1338+ _LIB .lbug_query_result_get_next_arrow_chunk (
1339+ ctypes .byref (self ._result ),
1340+ chunk_size ,
1341+ ctypes .byref (array_ptr ),
1342+ ),
1343+ "Failed to export Arrow chunk" ,
1344+ )
1345+ batches .append (
1346+ pa .RecordBatch ._import_from_c (ctypes .addressof (array_ptr ), schema )
1347+ )
1348+ if not batches :
1349+ return pa .Table .from_batches ([], schema = schema )
1350+ table = pa .Table .from_batches (batches , schema = schema )
1351+ if fallback_extension_types :
1352+ for idx , field in enumerate (table .schema ):
1353+ if str (field .type ) == "extension<arrow.uuid>" :
1354+ values = [
1355+ None if value is None else str (value )
1356+ for value in table .column (idx ).to_pylist ()
1357+ ]
1358+ table = table .set_column (
1359+ idx , field .name , pa .array (values , type = pa .string ())
1360+ )
1361+ return table
1362+ finally :
1363+ self .resetIterator ()
12311364
12321365 def getCSR (self , * _args : Any , ** _kwargs : Any ) -> Any :
1233- raise NotImplementedError ("CSR export is not yet implemented in C-API backend" )
1366+ import pyarrow as pa
1367+
1368+ column_names = self .getColumnNames ()
1369+ rows = self ._get_all_rows_from_start ()
1370+ if len (column_names ) == 2 and all (
1371+ name .endswith (".rowid" ) for name in column_names
1372+ ):
1373+ has_edge_ids = False
1374+ src_idx , edge_idx , dst_idx = 0 , None , 1
1375+ elif len (column_names ) >= 3 and all (
1376+ name .endswith (".rowid" ) for name in column_names [:3 ]
1377+ ):
1378+ has_edge_ids = True
1379+ src_idx , edge_idx , dst_idx = 0 , 1 , 2
1380+ else :
1381+ msg = "CSR export is only supported for rowid projections"
1382+ raise RuntimeError (msg )
1383+
1384+ max_src = max ((int (row [src_idx ]) for row in rows ), default = - 1 )
1385+ grouped : list [list [tuple [int | None , int ]]] = [[] for _ in range (max_src + 1 )]
1386+ for row in rows :
1387+ src = int (row [src_idx ])
1388+ edge = int (row [edge_idx ]) if edge_idx is not None else None
1389+ dst = int (row [dst_idx ])
1390+ grouped [src ].append ((edge , dst ))
1391+
1392+ indptr = [0 ]
1393+ indices : list [int ] = []
1394+ edge_ids : list [int ] = []
1395+ for entries in grouped :
1396+ for edge , dst in entries :
1397+ indices .append (dst )
1398+ if edge is not None :
1399+ edge_ids .append (edge )
1400+ indptr .append (len (indices ))
1401+
1402+ return {
1403+ "indptr" : pa .array (indptr , type = pa .int64 ()),
1404+ "indices" : pa .array (indices , type = pa .int64 ()),
1405+ "edge_ids" : pa .array (edge_ids , type = pa .int64 ()) if has_edge_ids else None ,
1406+ }
12341407
12351408 def getAsDF (self ) -> Any :
1236- raise NotImplementedError (
1237- "DataFrame export is not yet implemented in C-API backend"
1409+ import pandas as pd
1410+
1411+ df = pd .DataFrame (
1412+ self ._get_all_rows_from_start (), columns = self .getColumnNames ()
12381413 )
1414+ for name , dtype in zip (
1415+ self .getColumnNames (), self .getColumnDataTypes (), strict = False
1416+ ):
1417+ if name not in df :
1418+ continue
1419+ if dtype == "BOOL" :
1420+ df [name ] = df [name ].astype ("bool" )
1421+ elif dtype in {"INT8" , "INT16" , "INT32" , "INT64" , "SERIAL" }:
1422+ df [name ] = df [name ].astype (
1423+ {
1424+ "INT8" : "int8" ,
1425+ "INT16" : "int16" ,
1426+ "INT32" : "int32" ,
1427+ "INT64" : "int64" ,
1428+ "SERIAL" : "int64" ,
1429+ }[dtype ]
1430+ )
1431+ elif dtype in {"UINT8" , "UINT16" , "UINT32" , "UINT64" }:
1432+ df [name ] = df [name ].astype (
1433+ {
1434+ "UINT8" : "uint8" ,
1435+ "UINT16" : "uint16" ,
1436+ "UINT32" : "uint32" ,
1437+ "UINT64" : "uint64" ,
1438+ }[dtype ]
1439+ )
1440+ elif dtype == "FLOAT" :
1441+ df [name ] = df [name ].astype ("float32" )
1442+ elif dtype == "DOUBLE" :
1443+ df [name ] = df [name ].astype ("float64" )
1444+ elif dtype == "DATE" or dtype .startswith ("TIMESTAMP" ):
1445+ datetime_col = pd .to_datetime (df [name ])
1446+ if getattr (datetime_col .dt , "tz" , None ) is not None :
1447+ datetime_col = datetime_col .dt .tz_convert ("UTC" ).dt .tz_localize (
1448+ None
1449+ )
1450+ df [name ] = datetime_col .astype ("datetime64[us]" )
1451+ elif dtype == "INTERVAL" :
1452+ df [name ] = pd .to_timedelta (df [name ])
1453+ elif dtype == "INT128" :
1454+ df [name ] = df [name ].astype ("float64" )
1455+ return df
1456+
1457+ def _get_all_rows_from_start (self ) -> list [list [Any ]]:
1458+ self .resetIterator ()
1459+ rows = []
1460+ while self .hasNext ():
1461+ rows .append (self .getNext ())
1462+ self .resetIterator ()
1463+ return rows
12391464
12401465 def _convert_value (self , value : _LbugValue ) -> Any :
12411466 if _LIB .lbug_value_is_null (ctypes .byref (value )):
@@ -1764,6 +1989,7 @@ def _convert_value(self, value: _LbugValue) -> Any:
17641989class Connection :
17651990 def __init__ (self , database : Database , num_threads : int = 0 ):
17661991 self ._connection = _LbugConnection ()
1992+ self ._query_timeout_ms = 0
17671993 _check_state (
17681994 _LIB .lbug_connection_init (
17691995 ctypes .byref (database ._database ), ctypes .byref (self ._connection )
@@ -1795,14 +2021,33 @@ def set_query_timeout(self, timeout_in_ms: int) -> None:
17952021 ),
17962022 "Failed to set query timeout" ,
17972023 )
2024+ self ._query_timeout_ms = int (timeout_in_ms )
17982025
17992026 def interrupt (self ) -> None :
18002027 _LIB .lbug_connection_interrupt (ctypes .byref (self ._connection ))
18012028
2029+ def _call_with_timeout (self , callback : Any ) -> Any :
2030+ timer = None
2031+ if self ._query_timeout_ms > 0 :
2032+ timer = threading .Timer (
2033+ min (self ._query_timeout_ms / 1000 , 0.01 ), self .interrupt
2034+ )
2035+ timer .daemon = True
2036+ timer .start ()
2037+ try :
2038+ return callback ()
2039+ finally :
2040+ if timer is not None :
2041+ timer .cancel ()
2042+
18022043 def query (self , query : str ) -> QueryResult :
18032044 result = _LbugQueryResult ()
1804- state = _LIB .lbug_connection_query (
1805- ctypes .byref (self ._connection ), query .encode ("utf-8" ), ctypes .byref (result )
2045+ state = self ._call_with_timeout (
2046+ lambda : _LIB .lbug_connection_query (
2047+ ctypes .byref (self ._connection ),
2048+ query .encode ("utf-8" ),
2049+ ctypes .byref (result ),
2050+ )
18062051 )
18072052
18082053 # Query failures are commonly surfaced on QueryResult itself (isSuccess + getErrorMessage).
@@ -1836,10 +2081,12 @@ def execute(
18362081 if parameters :
18372082 prepared_statement .bind_parameters (parameters )
18382083 result = _LbugQueryResult ()
1839- state = _LIB .lbug_connection_execute (
1840- ctypes .byref (self ._connection ),
1841- ctypes .byref (prepared_statement ._prepared ),
1842- ctypes .byref (result ),
2084+ state = self ._call_with_timeout (
2085+ lambda : _LIB .lbug_connection_execute (
2086+ ctypes .byref (self ._connection ),
2087+ ctypes .byref (prepared_statement ._prepared ),
2088+ ctypes .byref (result ),
2089+ )
18432090 )
18442091
18452092 if state != _LBUG_SUCCESS and not result ._query_result :
0 commit comments