Skip to content

Commit f4c08f3

Browse files
authored
Merge pull request #1 from quackscience/features
Additional Features
2 parents 484d075 + 04b364b commit f4c08f3

File tree

2 files changed

+88
-4
lines changed

2 files changed

+88
-4
lines changed

main.py

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import re
23
import duckdb
34
import json
45
import time
@@ -8,10 +9,17 @@
89

910
from flask import Flask, request, jsonify
1011
from flask_httpauth import HTTPBasicAuth
12+
from flask_cors import CORS
13+
from cachetools import LRUCache
1114

1215
app = Flask(__name__, static_folder="public", static_url_path="")
1316
auth = HTTPBasicAuth()
17+
CORS(app)
1418

19+
# Initialize LRU Cache
20+
cache = LRUCache(maxsize=10)
21+
22+
# Default path for temp databases
1523
dbpath = 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+
68114
def 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
123181
def 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

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
Flask
22
flask_httpauth
3+
flask-cors
4+
cachetools
35
duckdb

0 commit comments

Comments
 (0)