11import os
2+ import re
23import duckdb
34import json
45import time
89
910from flask import Flask , request , jsonify
1011from flask_httpauth import HTTPBasicAuth
12+ from flask_cors import CORS
13+ from cachetools import LRUCache
1114
1215app = Flask (__name__ , static_folder = "public" , static_url_path = "" )
1316auth = HTTPBasicAuth ()
17+ CORS (app )
1418
19+ # Initialize LRU Cache
20+ cache = LRUCache (maxsize = 10 )
21+
22+ # Default path for temp databases
1523dbpath = os .getenv ('DBPATH' , '/tmp/' )
1624
1725# Global connection
@@ -65,6 +73,44 @@ def convert_to_clickhouse_jsoncompact(result, query_time):
6573
6674 return json .dumps (json_result )
6775
76+ def convert_to_clickhouse_json (result , query_time ):
77+ columns = result .description
78+ data = result .fetchall ()
79+
80+ meta = [{"name" : col [0 ], "type" : col [1 ]} for col in columns ]
81+
82+ data_list = []
83+ for row in data :
84+ row_dict = {columns [i ][0 ]: row [i ] for i in range (len (columns ))}
85+ data_list .append (row_dict )
86+
87+ json_result = {
88+ "meta" : meta ,
89+ "data" : data_list ,
90+ "rows" : len (data ),
91+ "statistics" : {
92+ "elapsed" : query_time ,
93+ "rows_read" : len (data ),
94+ "bytes_read" : sum (len (str (item )) for row in data for item in row )
95+ }
96+ }
97+
98+ return json .dumps (json_result )
99+
100+ def convert_to_csv_tsv (result , delimiter = ',' ):
101+ columns = result .description
102+ data = result .fetchall ()
103+
104+ lines = []
105+ header = delimiter .join ([col [0 ] for col in columns ])
106+ lines .append (header )
107+
108+ for row in data :
109+ line = delimiter .join ([str (item ) for item in row ])
110+ lines .append (line )
111+
112+ return '\n ' .join (lines ).encode ()
113+
68114def handle_insert_query (query , format , data = None ):
69115 table_name = query .split ("INTO" )[1 ].split ()[0 ].strip ()
70116
@@ -90,10 +136,8 @@ def save_to_tempfile(data):
90136 temp_file .close ()
91137 return temp_file .name
92138
93-
94- def duckdb_query_with_errmsg (query , format , data = None , request_method = "GET" ):
139+ def duckdb_query_with_errmsg (query , format = 'JSONCompact' , data = None , request_method = "GET" ):
95140 try :
96-
97141 if request_method == "POST" and query .strip ().lower ().startswith ('insert into' ) and data :
98142 return handle_insert_query (query , format , data )
99143
@@ -103,10 +147,14 @@ def duckdb_query_with_errmsg(query, format, data=None, request_method="GET"):
103147
104148 if format .lower () == 'jsoncompact' :
105149 output = convert_to_clickhouse_jsoncompact (result , query_time )
150+ elif format .lower () == 'json' :
151+ output = convert_to_clickhouse_json (result , query_time )
106152 elif format .lower () == 'jsoneachrow' :
107153 output = convert_to_ndjson (result )
108154 elif format .lower () == 'tsv' :
109- output = result .df ().to_csv (sep = '\t ' , index = False )
155+ output = convert_to_csv_tsv (result , delimiter = '\t ' )
156+ elif format .lower () == 'csv' :
157+ output = convert_to_csv_tsv (result , delimiter = ',' )
110158 else :
111159 output = result .fetchall ()
112160
@@ -118,17 +166,36 @@ def duckdb_query_with_errmsg(query, format, data=None, request_method="GET"):
118166 except Exception as e :
119167 return b"" , str (e ).encode ()
120168
169+ def sanitize_query (query ):
170+ pattern = re .compile (r"(?i)\s*FORMAT\s+(\w+)\s*" )
171+ match = re .search (pattern , query )
172+ if match :
173+ format_value = match .group (1 ).lower ()
174+ query = re .sub (pattern , ' ' , query ).strip ()
175+ return query , format_value .lower ()
176+ return query , None
177+
178+
121179@app .route ('/' , methods = ["GET" , "HEAD" ])
122180@auth .login_required
123181def clickhouse ():
124182 query = request .args .get ('query' , default = "" , type = str )
125183 format = request .args .get ('default_format' , default = "JSONCompact" , type = str )
126184 database = request .args .get ('database' , default = "" , type = str )
185+ query_id = request .args .get ('query_id' , default = None , type = str )
127186 data = None
128187
188+ query , sanitized_format = sanitize_query (query )
189+ if sanitized_format :
190+ format = sanitized_format
191+
129192 # Log incoming request data for debugging
130193 print (f"Received request: method={ request .method } , query={ query } , format={ format } , database={ database } " )
131194
195+ if query_id is not None and not query :
196+ if query_id in cache :
197+ return cache [query_id ], 200
198+
132199 if not query :
133200 return app .send_static_file ('play.html' )
134201
@@ -140,6 +207,10 @@ def clickhouse():
140207
141208 # Execute the query and capture the result and error message
142209 result , errmsg = duckdb_query_with_errmsg (query .strip (), format , data , request .method )
210+
211+ # Cache the result if query_id is provided
212+ if query_id and len (errmsg ) == 0 :
213+ cache [query_id ] = result
143214
144215 # Handle response for HEAD requests
145216 if len (errmsg ) == 0 :
@@ -171,6 +242,11 @@ def play():
171242 body = request .get_data () or None
172243 format = request .args .get ('default_format' , default = "JSONCompact" , type = str )
173244 database = request .args .get ('database' , default = "" , type = str )
245+ query_id = request .args .get ('query_id' , default = None , type = str )
246+
247+ if query_id is not None and not query :
248+ if query_id in cache :
249+ return cache [query_id ], 200
174250
175251 if query is None :
176252 query = ""
@@ -185,6 +261,12 @@ def play():
185261 if database :
186262 query = f"ATTACH '{ database } ' AS db; USE db; { query } "
187263
264+ query , sanitized_format = sanitize_query (query )
265+ if sanitized_format :
266+ format = sanitized_format
267+
268+ print ("DEBUG POST" , query , format )
269+
188270 result , errmsg = duckdb_query_with_errmsg (query .strip (), format )
189271 if len (errmsg ) == 0 :
190272 return result , 200
0 commit comments