@@ -187,31 +187,39 @@ def _run_query(self, query):
187187 """
188188 local_params = self .params .copy ()
189189 local_params .insert (1 , query )
190+ output = {}
191+
190192 try :
191193 with subprocess .Popen ([self .bin_path ] + local_params ,
192- stdout = subprocess .PIPE , stderr = subprocess .PIPE ) as iqlPopen : # Capturing stderr separately
194+ stdout = subprocess .PIPE , stderr = subprocess .PIPE ) as iqlPopen :
193195 stdout , stderr = iqlPopen .communicate ()
196+
194197 if self .debug :
195198 self ._debug_log (f"Query: { query } " )
196199 self ._debug_log (f"stdout: { stdout } " )
197200 self ._debug_log (f"stderr: { stderr } " )
201+
202+ # Check if stderr exists
198203 if stderr :
199- # Prioritizing stderr since that’s where the expected messages seem to be
200- return stderr .decode ('utf-8' ) if isinstance (stderr , bytes ) else str (stderr )
201- else :
202- # Here, we may consider concatenating stdout and stderr, or handling them separately based on the use case
203- return stdout .decode ('utf-8' ) if isinstance (stdout , bytes ) else str (stdout )
204+ output ["error" ] = stderr .decode ('utf-8' ) if isinstance (stderr , bytes ) else str (stderr )
205+
206+ # Check if theres data
207+ if stdout :
208+ output ["data" ] = stdout .decode ('utf-8' ) if isinstance (stdout , bytes ) else str (stdout )
209+
204210 except FileNotFoundError :
205- return "ERROR %s not found" % self . bin_path
211+ output [ "exception" ] = f "ERROR: { self . bin_path } not found"
206212 except Exception as e :
207- if 'stdout' in locals () and 'stderr' in locals ():
208- return f"ERROR: { str (e )} { e .__doc__ } , PARAMS: { local_params } , STDOUT: { stdout } , STDERR: { stderr } "
209- if 'stdout' in locals () and 'stderr' not in locals ():
210- return f"ERROR: { str (e )} { e .__doc__ } , PARAMS: { local_params } ,STDOUT: { stdout } "
211- elif 'stderr' in locals ():
212- return f"ERROR: { str (e )} { e .__doc__ } , PARAMS: { local_params } ,STDERR: { stderr } "
213- else :
214- return f"ERROR: { str (e )} { e .__doc__ } , PARAMS: { local_params } "
213+ error_details = {
214+ "exception" : str (e ),
215+ "doc" : e .__doc__ ,
216+ "params" : local_params ,
217+ "stdout" : stdout .decode ('utf-8' ) if 'stdout' in locals () and isinstance (stdout , bytes ) else "" ,
218+ "stderr" : stderr .decode ('utf-8' ) if 'stderr' in locals () and isinstance (stderr , bytes ) else ""
219+ }
220+ output ["exception" ] = f"ERROR: { json .dumps (error_details )} "
221+
222+ return output
215223
216224 def __init__ (self ,
217225 server_mode = False ,
@@ -474,70 +482,82 @@ def executeStmt(self, query):
474482 else :
475483 return result
476484 else :
477- result_msg = self ._run_query (query )
485+ result = self ._run_query (query )
486+ if "exception" in result :
487+ return {"error" : result ["exception" ]}
488+
489+ # message on stderr
490+ message = result ["error" ]
491+
478492 if self .output == 'pandas' :
479- return pd .DataFrame ({'message' : [result_msg ]})
493+ return pd .DataFrame ({'message' : [message ]}) if message else pd . DataFrame ({ 'message' : [ ]})
480494 elif self .output == 'csv' :
481- return result_msg
495+ return message
482496 else :
483- return [{'message' : result_msg }]
497+ return [{'message' : message }]
484498
485- def execute (self , query ):
486- """Executes a query using the StackQL instance and returns the output
487- in the format specified by the ` output` attribute .
499+ def execute (self , query , suppress_errors = True ):
500+ """
501+ Executes a StackQL query and returns the output based on the specified output format .
488502
489- Depending on the `server_mode` and `output` attribute of the instance,
490- this method either runs the query against the StackQL server or executes
491- it locally using a subprocess, returning the data in a dictionary, Pandas
492- DataFrame, or CSV format.
503+ This method supports execution both in server mode and locally using subprocess. In server mode,
504+ the query is sent to a StackQL server, while in local mode, it runs the query using a local binary.
493505
494- :param query: The StackQL query string to be executed.
495- :type query: str
506+ Args:
507+ query (str): The StackQL query string to be executed.
508+ suppress_errors (bool, optional): If set to True, the method will return an empty list if an error occurs.
496509
497- :return: The output result of the query. Depending on the `output` attribute,
498- the result can be a dictionary, a Pandas DataFrame, or a raw CSV string.
499- CSV output is currently not supported in `server_mode`.
500- :rtype: dict, pd.DataFrame, or str
510+ Returns:
511+ dict, pd.DataFrame, or str: The output of the query, which can be a dictionary, a Pandas DataFrame,
512+ or a raw CSV string, depending on the configured output format.
501513
502- Example:
503- >>> from pystackql import StackQL
514+ Raises:
515+ ValueError: If an unsupported output format is specified.
516+
517+ Examples:
504518 >>> stackql = StackQL()
505- >>> stackql_query = \" \" \" SELECT SPLIT_PART(machineType, '/', -1) as machine_type,
506- ... status, COUNT(*) as num_instances
507- ... FROM google.compute.instances
508- ... WHERE project = 'stackql-demo'
509- ... AND zone = 'australia-southeast1-a'
510- ... GROUP BY machine_type, status
511- ... HAVING COUNT(*) > 2\" \" \"
512- >>> result = stackql.execute(stackql_query)
519+ >>> query = '''
520+ SELECT SPLIT_PART(machineType, '/', -1) as machine_type, status, COUNT(*) as num_instances
521+ FROM google.compute.instances
522+ WHERE project = 'stackql-demo' AND zone = 'australia-southeast1-a'
523+ GROUP BY machine_type, status HAVING COUNT(*) > 2
524+ '''
525+ >>> result = stackql.execute(query)
513526 """
514527 if self .server_mode :
515- # Use server mode
516528 result = self ._run_server_query (query )
517-
518529 if self .output == 'pandas' :
519530 json_str = json .dumps (result )
520531 return pd .read_json (StringIO (json_str ))
521532 elif self .output == 'csv' :
522533 raise ValueError ("CSV output is not supported in server_mode." )
523534 else : # Assume 'dict' output
524535 return result
525-
526536 else :
527- # Local mode handling (existing logic)
528537 output = self ._run_query (query )
529- if self .output == 'csv' :
530- return output
531- elif self .output == 'pandas' :
532- try :
533- return pd .read_json (StringIO (output ))
534- except ValueError :
535- return pd .DataFrame ([{"error" : "Invalid JSON output: {}" .format (output .strip ())}])
536- else : # Assume 'dict' output
537- try :
538- return json .loads (output )
539- except ValueError :
540- return [{"error" : "Invalid JSON output: {}" .format (output .strip ())}]
538+ if "exception" in output :
539+ return {"error" : output ["exception" ]}
540+
541+ if "data" in output :
542+ # theres data, return it
543+ if self .output == 'csv' :
544+ return output ["data" ]
545+ elif self .output == 'pandas' :
546+ try :
547+ return pd .read_json (StringIO (output ["data" ]))
548+ except ValueError :
549+ return pd .DataFrame ([{"error" : "Invalid JSON output" }])
550+ else : # Assume 'dict' output
551+ try :
552+ return json .loads (output ["data" ])
553+ except ValueError :
554+ return {"error" : "Invalid JSON output" }
555+ else :
556+ if "error" in output :
557+ if suppress_errors :
558+ return []
559+ else :
560+ return {"error" : output ["error" ]}
541561
542562 #
543563 # asnyc query support
@@ -586,7 +606,13 @@ def _sync_query(self, query, new_connection=False):
586606 result = self ._run_server_query (query ) # Assuming this is a method that exists
587607 else :
588608 # Convert the JSON string to a Python object (list of dicts).
589- result = json .loads (self ._run_query (query ))
609+ query_results = self ._run_query (query )
610+ if "exception" in query_results :
611+ result = [{"error" : query_results ["exception" ]}]
612+ if "error" in query_results :
613+ result = [{"error" : query_results ["error" ]}]
614+ if "data" in query_results :
615+ result = json .loads (query_results ["data" ])
590616 # Convert the result to a DataFrame if necessary.
591617 if self .output == 'pandas' :
592618 return pd .DataFrame (result )
0 commit comments