diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfb8234 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.py[cod] +.venv/ +.env diff --git a/__pycache__/codegen.cpython-313.pyc b/__pycache__/codegen.cpython-313.pyc deleted file mode 100644 index b3f05c8..0000000 Binary files a/__pycache__/codegen.cpython-313.pyc and /dev/null differ diff --git a/__pycache__/phi_parser.cpython-312.pyc b/__pycache__/phi_parser.cpython-312.pyc deleted file mode 100644 index 1d17fc3..0000000 Binary files a/__pycache__/phi_parser.cpython-312.pyc and /dev/null differ diff --git a/__pycache__/phi_parser.cpython-313.pyc b/__pycache__/phi_parser.cpython-313.pyc deleted file mode 100644 index f3d9ad6..0000000 Binary files a/__pycache__/phi_parser.cpython-313.pyc and /dev/null differ diff --git a/codegen.py b/codegen.py index d9a36e1..1a5a96b 100644 --- a/codegen.py +++ b/codegen.py @@ -204,8 +204,6 @@ def _emit_filter(spec: PhiSpec) -> str: or_clauses.append("" + " and ".join(and_group) + "") condition = " or ".join(or_clauses) - if len(condition.strip()) == 0: - condition = True lines.append(f" if ({condition}):") lines.append(" filtered_mf_struct[_key] = entry") @@ -234,4 +232,4 @@ def main() -> None: if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/phi_input_case2.txt b/phi_input_case2.txt index 9334778..e4ea0ad 100644 --- a/phi_input_case2.txt +++ b/phi_input_case2.txt @@ -1,18 +1,18 @@ -# Case 2: Count and Max quant per customer """ -SELECT - cust, - COUNT(*), - MAX(quant) -FROM sales -WHERE quant IS NOT NULL -GROUP BY cust; +select state, month, a_sum_quant, b_avg_quant +from sales +where year = 2018 +group by state, month; a, b +such that a_state = state and a_month = month + and b_state = state and b_month = month +having a_sum_quant >= 3 * b_avg_quant """ -S: cust, 1_count_*, 1_max_quant -n: 1 -V: cust -F: 1_count_*, 1_max_quant +S: state, month, 1_sum_quant, 2_avg_quant +n: 2 +V: state, month +F: 1_sum_quant, 2_avg_quant sigma: - 1: (cust == g_cust) and (quant is not None) -G: \ No newline at end of file + 1: state == g_state and month == g_month and year == 2018 + 2: state == g_state and month == g_month and year == 2018 +G: 1_sum_quant >= 3 * 2_avg_quant diff --git a/phi_input_case3.txt b/phi_input_case3.txt index 34f6b5f..8b3da39 100644 --- a/phi_input_case3.txt +++ b/phi_input_case3.txt @@ -1,30 +1,11 @@ """ -WITH a AS ( - SELECT - state, - month, - SUM(quant) AS a_sum_quant - FROM sales - WHERE state = 'CA' - GROUP BY state, month -), -b AS ( - SELECT - state, - MAX(quant) AS b_max_quant - FROM sales - GROUP BY state -) -SELECT - a.state, - a.month, - a.a_sum_quant, - b.b_max_quant -FROM a -JOIN b - ON a.state = b.state -WHERE a.a_sum_quant >= b.b_max_quant * 2; - +select state, month, a_sum_quant, b_max_quant +from sales +where state = 'CA' +group by state,month; a,b +such that a_state = state and a_month = month + and b_state = state +having a_sum_quant >= b_max_quant * 2 """ S: state, month, 1_sum_quant, 2_max_quant n: 2 diff --git a/phi_parser.py b/phi_parser.py index ca7be18..5462f2e 100644 --- a/phi_parser.py +++ b/phi_parser.py @@ -118,7 +118,6 @@ def parse_phi_file(path: str) -> PhiSpec: grouping_attrs = _split_list(V) aggs: List[AggSpec] = [] - having: List[List[str]] = [] if F: for item in _split_list(F): m = _AGG_RE.match(item) @@ -152,6 +151,8 @@ def parse_phi_file(path: str) -> PhiSpec: # Split OR (case-insensitive) or_blocks = re.split(r'\s+OR\s+', G, flags=re.IGNORECASE) + having: List[List[str]] = [] + for block in or_blocks: and_parts = re.split(r'\s+AND\s+', block, flags=re.IGNORECASE) rewrite = [] @@ -197,5 +198,4 @@ def _split_list(s: str) -> List[str]: def _first_token(s: str) -> str: - return s.strip().split()[0] - + return s.strip().split()[0] \ No newline at end of file diff --git a/qpe_case1.py b/qpe_case1.py new file mode 100644 index 0000000..d05a097 --- /dev/null +++ b/qpe_case1.py @@ -0,0 +1,97 @@ +from __future__ import annotations +import os +import psycopg2 +import psycopg2.extras +from dotenv import load_dotenv + +# Auto-generated by codegen.py. Re-generate anytime. + +def _safe_eval_predicate(expr: str, env: dict) -> bool: + """ + English comments: + Evaluate a predicate expression in a restricted environment. + - Variables are taken from the given env dict. + - Supports Python boolean logic (and/or/not). + """ + if not expr: + return True + return bool(eval(expr, {"__builtins__": {}}, env)) + + +def run_query(): + try: + load_dotenv() + user = os.getenv("USER") + password = os.getenv("PASSWORD") + dbname = os.getenv("DBNAME") + host = os.getenv("HOST", "localhost") + port = os.getenv("PORT", "5432") + + conn = psycopg2.connect( + dbname=dbname, + user=user, + password=password, + host=host, + port=port, + cursor_factory=psycopg2.extras.DictCursor + ) + cur = conn.cursor() + + except: + raise RuntimeError("Incorrect USER/PASSWORD/DBNAME in .env") + + # mf_struct maps grouping key tuple -> entry dict + mf_struct = {} + + # SCAN 0: initialize mf_struct entries for distinct grouping keys + cur.execute('SELECT * FROM sales') + for row in cur: + key = (row['prod'], row['month']) + if key not in mf_struct: + entry = {} + entry['prod'] = row['prod'] + entry['month'] = row['month'] + entry['1_sum_quant'] = 0 + entry['2_sum_quant'] = 0 + mf_struct[key] = entry + + # SCAN 1: compute aggregates for grouping variable 1 + cur.execute('SELECT * FROM sales') + for row in cur: + for _key, entry in mf_struct.items(): + env = dict(row) + env['g_prod'] = entry.get('prod') + env['g_month'] = entry.get('month') + if not _safe_eval_predicate('prod == g_prod and month == g_month', env): + continue + entry['1_sum_quant'] += (row['quant'] if row['quant'] is not None else 0) + + # SCAN 2: compute aggregates for grouping variable 2 + cur.execute('SELECT * FROM sales') + for row in cur: + for _key, entry in mf_struct.items(): + env = dict(row) + env['g_prod'] = entry.get('prod') + env['g_month'] = entry.get('month') + if not _safe_eval_predicate('prod == g_prod', env): + continue + entry['2_sum_quant'] += (row['quant'] if row['quant'] is not None else 0) + + filtered_mf_struct = {} + for _key, entry in mf_struct.items(): + if (entry['1_sum_quant'] > 0.9*entry['2_sum_quant']/10 and (entry['1_sum_quant'] > 40000 or entry['2_sum_quant']<500000) and entry['month'] == 11): + filtered_mf_struct[_key] = entry + + # Output + out_cols = ['prod', 'month', '1_sum_quant', '2_sum_quant'] + print("\t".join(out_cols)) + for _key, entry in filtered_mf_struct.items(): + row_out = [str(entry.get(c, "")) for c in out_cols] + print("\t".join(row_out)) + + cur.close() + conn.close() + + +if __name__ == "__main__": + run_query() diff --git a/qpe_case2.py b/qpe_case2.py new file mode 100644 index 0000000..e71efc2 --- /dev/null +++ b/qpe_case2.py @@ -0,0 +1,103 @@ +from __future__ import annotations +import os +import psycopg2 +import psycopg2.extras +from dotenv import load_dotenv + +# Auto-generated by codegen.py. Re-generate anytime. + +def _safe_eval_predicate(expr: str, env: dict) -> bool: + """ + English comments: + Evaluate a predicate expression in a restricted environment. + - Variables are taken from the given env dict. + - Supports Python boolean logic (and/or/not). + """ + if not expr: + return True + return bool(eval(expr, {"__builtins__": {}}, env)) + + +def run_query(): + try: + load_dotenv() + user = os.getenv("USER") + password = os.getenv("PASSWORD") + dbname = os.getenv("DBNAME") + host = os.getenv("HOST", "localhost") + port = os.getenv("PORT", "5432") + + conn = psycopg2.connect( + dbname=dbname, + user=user, + password=password, + host=host, + port=port, + cursor_factory=psycopg2.extras.DictCursor + ) + cur = conn.cursor() + + except: + raise RuntimeError("Incorrect USER/PASSWORD/DBNAME in .env") + + # mf_struct maps grouping key tuple -> entry dict + mf_struct = {} + + # SCAN 0: initialize mf_struct entries for distinct grouping keys + cur.execute('SELECT * FROM sales') + for row in cur: + key = (row['state'], row['month']) + if key not in mf_struct: + entry = {} + entry['state'] = row['state'] + entry['month'] = row['month'] + entry['1_sum_quant'] = 0 + entry['2_avg_quant__sum'] = 0 + entry['2_avg_quant__count'] = 0 + entry['2_avg_quant'] = 0 + mf_struct[key] = entry + + # SCAN 1: compute aggregates for grouping variable 1 + cur.execute('SELECT * FROM sales') + for row in cur: + for _key, entry in mf_struct.items(): + env = dict(row) + env['g_state'] = entry.get('state') + env['g_month'] = entry.get('month') + if not _safe_eval_predicate('state == g_state and month == g_month and year == 2018', env): + continue + entry['1_sum_quant'] += (row['quant'] if row['quant'] is not None else 0) + + # SCAN 2: compute aggregates for grouping variable 2 + cur.execute('SELECT * FROM sales') + for row in cur: + for _key, entry in mf_struct.items(): + env = dict(row) + env['g_state'] = entry.get('state') + env['g_month'] = entry.get('month') + if not _safe_eval_predicate('state == g_state and month == g_month and year == 2018', env): + continue + val = row['quant'] + if val is not None: + entry['2_avg_quant__sum'] += val + entry['2_avg_quant__count'] += 1 + entry['2_avg_quant'] = entry['2_avg_quant__sum'] / entry['2_avg_quant__count'] + + filtered_mf_struct = {} + for _key, entry in mf_struct.items(): + if (entry['1_sum_quant'] >= 3 * entry['2_avg_quant']): + filtered_mf_struct[_key] = entry + + # Output + out_cols = ['state', 'month', '1_sum_quant', '2_avg_quant'] + print("\t".join(out_cols)) + for _key, entry in filtered_mf_struct.items(): + row_out = [str(entry.get(c, "")) for c in out_cols] + print("\t".join(row_out)) + + cur.close() + conn.close() + + +if __name__ == "__main__": + run_query() diff --git a/qpe_case3.py b/qpe_case3.py new file mode 100644 index 0000000..163c21c --- /dev/null +++ b/qpe_case3.py @@ -0,0 +1,100 @@ +from __future__ import annotations +import os +import psycopg2 +import psycopg2.extras +from dotenv import load_dotenv + +# Auto-generated by codegen.py. Re-generate anytime. + +def _safe_eval_predicate(expr: str, env: dict) -> bool: + """ + English comments: + Evaluate a predicate expression in a restricted environment. + - Variables are taken from the given env dict. + - Supports Python boolean logic (and/or/not). + """ + if not expr: + return True + return bool(eval(expr, {"__builtins__": {}}, env)) + + +def run_query(): + try: + load_dotenv() + user = os.getenv("USER") + password = os.getenv("PASSWORD") + dbname = os.getenv("DBNAME") + host = os.getenv("HOST", "localhost") + port = os.getenv("PORT", "5432") + + conn = psycopg2.connect( + dbname=dbname, + user=user, + password=password, + host=host, + port=port, + cursor_factory=psycopg2.extras.DictCursor + ) + cur = conn.cursor() + + except: + raise RuntimeError("Incorrect USER/PASSWORD/DBNAME in .env") + + # mf_struct maps grouping key tuple -> entry dict + mf_struct = {} + + # SCAN 0: initialize mf_struct entries for distinct grouping keys + cur.execute('SELECT * FROM sales') + for row in cur: + key = (row['state'], row['month']) + if key not in mf_struct: + entry = {} + entry['state'] = row['state'] + entry['month'] = row['month'] + entry['1_sum_quant'] = 0 + entry['2_max_quant'] = None + mf_struct[key] = entry + + # SCAN 1: compute aggregates for grouping variable 1 + cur.execute('SELECT * FROM sales') + for row in cur: + for _key, entry in mf_struct.items(): + env = dict(row) + env['g_state'] = entry.get('state') + env['g_month'] = entry.get('month') + if not _safe_eval_predicate('state == g_state and month == g_month', env): + continue + entry['1_sum_quant'] += (row['quant'] if row['quant'] is not None else 0) + + # SCAN 2: compute aggregates for grouping variable 2 + cur.execute('SELECT * FROM sales') + for row in cur: + for _key, entry in mf_struct.items(): + env = dict(row) + env['g_state'] = entry.get('state') + env['g_month'] = entry.get('month') + if not _safe_eval_predicate('state == g_state', env): + continue + val = row['quant'] + if val is not None: + if entry['2_max_quant'] is None or val > entry['2_max_quant']: + entry['2_max_quant'] = val + + filtered_mf_struct = {} + for _key, entry in mf_struct.items(): + if (entry['1_sum_quant'] >= entry['2_max_quant'] * 2 and entry['state'] == 'CA'): + filtered_mf_struct[_key] = entry + + # Output + out_cols = ['state', 'month', '1_sum_quant', '2_max_quant'] + print("\t".join(out_cols)) + for _key, entry in filtered_mf_struct.items(): + row_out = [str(entry.get(c, "")) for c in out_cols] + print("\t".join(row_out)) + + cur.close() + conn.close() + + +if __name__ == "__main__": + run_query()