11#!/usr/bin/env python3
2- """
3- Query runner for BerlinMOD benchmark
4- """
2+ """Query runner for BerlinMOD benchmark."""
53
64import subprocess
75import time
108import os
119import argparse
1210import re
13- from typing import Dict , Tuple
11+ from typing import Dict , Optional , Tuple
1412
13+ QUERIES_FILE = "./sql/berlinmod_r_queries.sql"
1514QUERIES_NUM = 17
1615
16+
17+ def _parse_queries (queries_file : str ) -> Dict [int , str ]:
18+ """Split berlinmod_r_queries.sql into {query_num: sql} by -- QN: markers."""
19+ with open (queries_file , encoding = "utf-8" ) as f :
20+ content = f .read ()
21+ parts = re .split (r"(?=^-- Q\d+:)" , content , flags = re .MULTILINE )
22+ queries : Dict [int , str ] = {}
23+ for part in parts :
24+ m = re .match (r"-- Q(\d+):" , part .strip ())
25+ if m :
26+ n = int (m .group (1 ))
27+ sql = re .sub (r"^-- Q\d+:.*\n" , "" , part .strip (), count = 1 ).strip ()
28+ queries [n ] = sql
29+ return queries
30+
31+
1732class QueryRunner :
18- def __init__ (self , duckdb_path : str = "../../build/release/duckdb" ,
19- benchmark : str = "default" ,
20- queries_path : str = "./sql/queries" ,
21- explain_path : str = "./sql/explain" ,
22- output_path : str = "./results/output" ):
33+ def __init__ (
34+ self ,
35+ duckdb_path : str = "../../build/release/duckdb" ,
36+ benchmark : str = "default" ,
37+ queries_file : str = QUERIES_FILE ,
38+ output_path : str = "./results/output" ,
39+ ):
2340 self .duckdb_path = duckdb_path
2441 self .benchmark = benchmark
25- self .queries_path = queries_path
26- self .explain_path = explain_path
42+ self .queries_file = queries_file
2743 self .output_path = output_path
2844 self .queries_num = QUERIES_NUM
45+ self ._queries : Optional [Dict [int , str ]] = None
46+
47+ def _get_queries (self ) -> Dict [int , str ]:
48+ if self ._queries is None :
49+ self ._queries = _parse_queries (self .queries_file )
50+ return self ._queries
51+
52+ def _run_sql (self , sql : str ) -> subprocess .CompletedProcess :
53+ return subprocess .run (
54+ [self .duckdb_path , f"./databases/{ self .benchmark } .db" ],
55+ input = sql ,
56+ capture_output = True ,
57+ text = True ,
58+ )
59+
60+ def run_query (self , query_num : int ) -> Tuple [float , int ]:
61+ queries = self ._get_queries ()
62+ if query_num not in queries :
63+ print (f"\t Error: Q{ query_num } not found in { self .queries_file } " )
64+ return - 1 , - 1
65+
66+ print (f"\n Running Q{ query_num } " )
67+ output_file = f"{ self .output_path } /{ self .benchmark } /query_{ query_num } .csv"
68+ full_sql = f".mode csv\n .output { output_file } \n { queries [query_num ]} "
2969
30- def run_sql (self , filename : str ) -> Tuple [float , int ]:
31- success = False
32- print (f"\n Running { filename } " )
3370 start_time = time .time ()
34- sql_path = os .path .join (self .queries_path , filename )
35- if not os .path .exists (sql_path ):
36- print (f"\t Error: Query file not found: { sql_path } " )
37- return - 1 , - 1
38-
39- with open (sql_path , "r" ) as f :
40- sql = f .read ()
41-
42- sql = sql .replace (".output results/output/query" , f".output results/output/{ self .benchmark } /query" )
43-
44- while not success :
45- result = subprocess .run (
46- [self .duckdb_path , f"./databases/{ self .benchmark } .db" ],
47- input = sql ,
48- capture_output = True ,
49- text = True
50- )
71+ while True :
72+ result = self ._run_sql (full_sql )
5173 if result .returncode == 0 :
52- success = True
53- else :
54- print (f"\t Error running query: { result .stderr } " )
55- print ("\t Trying again..." )
56- time .sleep (1 )
57- start_time = time .time ()
58-
59- end_time = time .time ()
60- elapsed = (end_time - start_time ) * 1000 # milliseconds
74+ break
75+ print (f"\t Error: { result .stderr } " )
76+ print ("\t Retrying..." )
77+ time .sleep (1 )
78+ start_time = time .time ()
6179
80+ elapsed = (time .time () - start_time ) * 1000
6281 print (f"\t Done in { elapsed :.2f} ms" )
6382
64- line_count = self .run_validation ( filename )
83+ line_count = self ._count_output_rows ( output_file )
6584 print (f"\t Output row count: { line_count } " )
66-
6785 return elapsed , line_count
6886
69- def run_validation (self , filename : str ) -> int :
70- output_file = f"{ self .output_path } /{ self .benchmark } /{ filename .replace ('.sql' , '.csv' )} "
87+ def _count_output_rows (self , output_file : str ) -> int :
7188 if not os .path .exists (output_file ):
72- print (f"\t Error: Output file not found: { output_file } " )
89+ print (f"\t Error: output file not found: { output_file } " )
7390 return - 1
74- with open (output_file , "r" ) as f :
75- line_count = sum (1 for _ in f )
76- if line_count > 0 :
77- line_count -= 1
78- return line_count
91+ with open (output_file , encoding = "utf-8" ) as f :
92+ count = sum (1 for _ in f )
93+ return max (0 , count - 1 )
7994
8095 def run_queries (self ) -> Dict :
81- results = dict ()
82-
83- for query_num in range (1 , self .queries_num + 1 ):
84- filename = f"query_{ query_num } .sql"
85- elapsed , line_count = self .run_sql (filename )
96+ results : Dict = {}
97+ for n in range (1 , self .queries_num + 1 ):
98+ elapsed , line_count = self .run_query (n )
8699 if elapsed != - 1 :
87- results [filename ] = {
100+ results [f"query_ { n } .sql" ] = {
88101 "elapsed" : elapsed ,
89- "row_count" : line_count
102+ "row_count" : line_count ,
90103 }
91-
92104 return results
93105
94- def run_explain_sql (self , filename : str ) -> Tuple [float , int ]:
95- output_file = f"{ self .output_path } /{ self .benchmark } /explain/{ filename .replace ('.sql' , '.txt' )} "
96-
97- success = False
98- print (f"\n Running { filename } " )
99- sql_path = os .path .join (self .explain_path , filename )
100- if not os .path .exists (sql_path ):
101- print (f"\t Error: Query file not found: { sql_path } " )
102- return - 1 , - 1
103-
104- with open (sql_path , "r" ) as f :
105- sql = f .read ()
106-
107- sql = sql .replace (".output results/output/explain" , f".output results/output/{ self .benchmark } /explain" )
108-
109- while not success :
110- result = subprocess .run (
111- [self .duckdb_path , f"./databases/{ self .benchmark } .db" ],
112- input = sql ,
113- capture_output = True ,
114- text = True
115- )
116- if result .returncode == 0 :
117- success = True
118- else :
119- print (f"\t Error running query: { result .stderr } " )
120- print ("\t Trying again..." )
121- time .sleep (1 )
122- start_time = time .time ()
123-
124- with open (output_file , "r" ) as f :
125- output = f .readlines ()
126-
127- elapsed = - 1.0
128- for line in output :
129- match = re .search (r"Total Time:\s*([0-9.]+)s" , line )
130- if match :
131- try :
132- elapsed = float (match .group (1 )) * 1000 # convert to ms
133- except ValueError :
134- elapsed = - 1.0
135- break
136-
137- print (f"\t Done in { elapsed :.2f} ms" )
138-
139- return elapsed
140-
141106 def run_explain (self ) -> Dict :
142- if not os .path .exists (f"./results/output/{ self .benchmark } /explain" ):
143- os .makedirs (f"./results/output/{ self .benchmark } /explain" )
144-
145- results = dict ()
146- for query_num in range (1 , self .queries_num + 1 ):
147- filename = f"query_{ query_num } .sql"
148- elapsed = self .run_explain_sql (filename )
149- if elapsed != - 1 :
150- results [filename ] = elapsed
107+ queries = self ._get_queries ()
108+ explain_dir = f"{ self .output_path } /{ self .benchmark } /explain"
109+ os .makedirs (explain_dir , exist_ok = True )
110+
111+ results : Dict = {}
112+ for n in range (1 , self .queries_num + 1 ):
113+ if n not in queries :
114+ continue
115+ output_file = f"{ explain_dir } /query_{ n } .txt"
116+ full_sql = (
117+ f".output { output_file } \n "
118+ f"SET memory_limit = '20GB';\n "
119+ f"EXPLAIN ANALYZE\n { queries [n ]} "
120+ )
121+ print (f"\n Running EXPLAIN Q{ n } " )
122+ result = self ._run_sql (full_sql )
123+ if result .returncode != 0 :
124+ print (f"\t Error: { result .stderr } " )
125+ continue
126+
127+ elapsed = - 1.0
128+ if os .path .exists (output_file ):
129+ with open (output_file , encoding = "utf-8" ) as f :
130+ for line in f :
131+ m = re .search (r"Total Time:\s*([0-9.]+)s" , line )
132+ if m :
133+ try :
134+ elapsed = float (m .group (1 )) * 1000
135+ except ValueError :
136+ pass
137+ break
138+
139+ print (f"\t Done in { elapsed :.2f} ms" )
140+ results [f"query_{ n } .sql" ] = elapsed
151141 return results
152142
153- def main ():
154- parser = argparse .ArgumentParser (description = "Data loader for BerlinMOD benchmark" )
155- parser .add_argument ("--benchmark" , type = str , required = True , help = "Name of the benchmark run" )
156- parser .add_argument ("--explain" , type = int , default = 0 , choices = [0 , 1 ], help = "Run explain queries" )
157- benchmark = parser .parse_args ().benchmark
158- explain = parser .parse_args ().explain
159143
160- if not os .path .exists (f"./results/output/{ benchmark } " ):
161- os .makedirs (f"./results/output/{ benchmark } " )
144+ def main ():
145+ parser = argparse .ArgumentParser (description = "BerlinMOD query runner for DuckDB" )
146+ parser .add_argument ("--benchmark" , type = str , required = True )
147+ parser .add_argument ("--explain" , type = int , default = 0 , choices = [0 , 1 ])
148+ args = parser .parse_args ()
162149
163150 duckdb_path = "../../build/release/duckdb"
164151 if not os .path .exists (duckdb_path ):
165- print (f"Error: DuckDB executable not found at { duckdb_path } " )
166- print ("Please make sure you're running this from the benchmark directory and DuckDB is built." )
152+ print (f"Error: DuckDB not found at { duckdb_path } " )
167153 sys .exit (1 )
168-
169- runner = QueryRunner (duckdb_path , benchmark )
170- if explain :
171- results = runner .run_explain ()
172- else :
173- results = runner .run_queries ()
174-
175- if not os .path .exists (f"./results/stats/{ benchmark } " ):
176- os .makedirs (f"./results/stats/{ benchmark } " )
177-
178- stats_filename = "run_queries.json" if not explain else "run_explain.json"
179- with open (f"./results/stats/{ benchmark } /{ stats_filename } " , "w" ) as f :
154+
155+ output_path = "./results/output"
156+ os .makedirs (f"{ output_path } /{ args .benchmark } " , exist_ok = True )
157+
158+ runner = QueryRunner (duckdb_path , args .benchmark )
159+ results = runner .run_explain () if args .explain else runner .run_queries ()
160+
161+ stats_dir = f"./results/stats/{ args .benchmark } "
162+ os .makedirs (stats_dir , exist_ok = True )
163+ stats_file = "run_explain.json" if args .explain else "run_queries.json"
164+ with open (f"{ stats_dir } /{ stats_file } " , "w" , encoding = "utf-8" ) as f :
180165 json .dump (results , f , indent = 4 )
181-
182- print ( f" \n Results saved to ./results/stats/ { benchmark } / { stats_filename } " )
166+ print ( f" \n Results saved to { stats_dir } / { stats_file } " )
167+
183168
184169if __name__ == "__main__" :
185- main ()
170+ main ()
0 commit comments