|
21 | 21 | This module provides SQL query capability via pypaimon-rust + DataFusion. |
22 | 22 | """ |
23 | 23 |
|
| 24 | +import os |
| 25 | +import re |
24 | 26 | import sys |
| 27 | +import time |
| 28 | + |
| 29 | +_PAIMON_BANNER = r""" |
| 30 | + ____ _ |
| 31 | + / __ \____ _(_)___ ___ ____ ____ |
| 32 | + / /_/ / __ `/ / __ `__ \/ __ \/ __ \ |
| 33 | + / ____/ /_/ / / / / / / / /_/ / / / / |
| 34 | +/_/ \__,_/_/_/ /_/ /_/\____/_/ /_/ |
| 35 | +
|
| 36 | + Powered by pypaimon-rust + DataFusion |
| 37 | + Type 'help' for usage, 'exit' to quit. |
| 38 | +""" |
| 39 | + |
| 40 | +_USE_PATTERN = re.compile( |
| 41 | + r"^\s*use\s+(\w+)\s*;?\s*$", |
| 42 | + re.IGNORECASE, |
| 43 | +) |
| 44 | + |
| 45 | +_SHOW_DATABASES_PATTERN = re.compile( |
| 46 | + r"^\s*show\s+databases\s*;?\s*$", |
| 47 | + re.IGNORECASE, |
| 48 | +) |
| 49 | + |
| 50 | +_SHOW_TABLES_PATTERN = re.compile( |
| 51 | + r"^\s*show\s+tables\s*;?\s*$", |
| 52 | + re.IGNORECASE, |
| 53 | +) |
| 54 | + |
| 55 | +_HISTORY_FILE = os.path.expanduser("~/.paimon_history") |
| 56 | +_HISTORY_MAX_LENGTH = 1000 |
| 57 | + |
| 58 | +_PROMPT = "paimon> " |
| 59 | +_CONTINUATION_PROMPT = " > " |
| 60 | + |
| 61 | + |
| 62 | +def _setup_readline(): |
| 63 | + """Enable readline for arrow key support and persistent command history.""" |
| 64 | + try: |
| 65 | + import readline |
| 66 | + readline.set_history_length(_HISTORY_MAX_LENGTH) |
| 67 | + if os.path.exists(_HISTORY_FILE): |
| 68 | + readline.read_history_file(_HISTORY_FILE) |
| 69 | + except ImportError: |
| 70 | + pass |
| 71 | + |
| 72 | + |
| 73 | +def _save_history(): |
| 74 | + """Save readline history to file.""" |
| 75 | + try: |
| 76 | + import readline |
| 77 | + readline.write_history_file(_HISTORY_FILE) |
| 78 | + except (ImportError, OSError): |
| 79 | + pass |
25 | 80 |
|
26 | 81 |
|
27 | 82 | def cmd_sql(args): |
@@ -59,43 +114,170 @@ def _execute_query(catalog, query, output_format): |
59 | 114 | print(f"Error: {e}", file=sys.stderr) |
60 | 115 | sys.exit(1) |
61 | 116 |
|
| 117 | + _print_result(result, output_format) |
| 118 | + |
| 119 | + |
| 120 | +def _print_result(result, output_format, elapsed=None): |
| 121 | + """Print a PyArrow Table in the requested format.""" |
| 122 | + df = result.to_pandas() |
62 | 123 | if output_format == 'json': |
63 | 124 | import json |
64 | | - df = result.to_pandas() |
65 | 125 | print(json.dumps(df.to_dict(orient='records'), ensure_ascii=False)) |
66 | 126 | else: |
67 | | - df = result.to_pandas() |
68 | 127 | print(df.to_string(index=False)) |
69 | 128 |
|
| 129 | + row_count = len(df) |
| 130 | + if elapsed is not None: |
| 131 | + print(f"({row_count} {'row' if row_count == 1 else 'rows'} in {elapsed:.2f}s)") |
| 132 | + else: |
| 133 | + print(f"({row_count} {'row' if row_count == 1 else 'rows'})") |
70 | 134 |
|
71 | | -def _interactive_repl(catalog, output_format): |
72 | | - """Run an interactive SQL REPL.""" |
73 | | - print("Paimon SQL (powered by pypaimon-rust + DataFusion)") |
74 | | - print("Type your SQL queries. Use Ctrl+D or 'exit' to quit.\n") |
75 | 135 |
|
| 136 | +def _read_multiline_query(): |
| 137 | + """Read a potentially multi-line SQL query, terminated by ';'. |
| 138 | +
|
| 139 | + Returns the complete query string, or None on EOF/interrupt. |
| 140 | + """ |
| 141 | + lines = [] |
| 142 | + prompt = _PROMPT |
76 | 143 | while True: |
77 | 144 | try: |
78 | | - query = input("paimon> ").strip() |
| 145 | + line = input(prompt) |
79 | 146 | except (EOFError, KeyboardInterrupt): |
80 | | - print() |
81 | | - break |
| 147 | + if lines: |
| 148 | + # Cancel current multi-line input |
| 149 | + print() |
| 150 | + return "" |
| 151 | + return None |
| 152 | + |
| 153 | + lines.append(line) |
| 154 | + joined = "\n".join(lines).strip() |
82 | 155 |
|
83 | | - if not query: |
| 156 | + if not joined: |
| 157 | + lines.clear() |
| 158 | + prompt = _PROMPT |
84 | 159 | continue |
85 | | - if query.lower() in ('exit', 'quit'): |
86 | | - break |
87 | 160 |
|
88 | | - try: |
89 | | - result = catalog.sql(query) |
90 | | - df = result.to_pandas() |
91 | | - if output_format == 'json': |
92 | | - import json |
93 | | - print(json.dumps(df.to_dict(orient='records'), ensure_ascii=False)) |
94 | | - else: |
95 | | - print(df.to_string(index=False)) |
96 | | - print() |
97 | | - except Exception as e: |
98 | | - print(f"Error: {e}\n", file=sys.stderr) |
| 161 | + # Single-word commands that don't need ';' |
| 162 | + lower = joined.lower().rstrip(';').strip() |
| 163 | + if lower in ('exit', 'quit', 'help'): |
| 164 | + return joined |
| 165 | + |
| 166 | + # USE / SHOW commands don't strictly need ';' either |
| 167 | + if _USE_PATTERN.match(joined) or _SHOW_DATABASES_PATTERN.match(joined) or _SHOW_TABLES_PATTERN.match(joined): |
| 168 | + return joined |
| 169 | + |
| 170 | + # For SQL statements, wait for ';' |
| 171 | + if joined.endswith(';'): |
| 172 | + return joined |
| 173 | + |
| 174 | + prompt = _CONTINUATION_PROMPT |
| 175 | + |
| 176 | + |
| 177 | +def _handle_use(catalog, match): |
| 178 | + """Handle USE <database> command.""" |
| 179 | + database = match.group(1) |
| 180 | + try: |
| 181 | + catalog._get_sql_context().use_database(database) |
| 182 | + print(f"Using database '{database}'.") |
| 183 | + except Exception as e: |
| 184 | + print(f"Error: {e}", file=sys.stderr) |
| 185 | + |
| 186 | + |
| 187 | +def _handle_show_databases(catalog): |
| 188 | + """Handle SHOW DATABASES command.""" |
| 189 | + try: |
| 190 | + databases = catalog.list_databases() |
| 191 | + for db in databases: |
| 192 | + print(db) |
| 193 | + except Exception as e: |
| 194 | + print(f"Error: {e}", file=sys.stderr) |
| 195 | + |
| 196 | + |
| 197 | +def _handle_show_tables(catalog): |
| 198 | + """Handle SHOW TABLES command.""" |
| 199 | + try: |
| 200 | + ctx = catalog._get_sql_context() |
| 201 | + database = ctx._default_database |
| 202 | + tables = catalog.list_tables(database) |
| 203 | + for table in tables: |
| 204 | + print(table) |
| 205 | + except Exception as e: |
| 206 | + print(f"Error: {e}", file=sys.stderr) |
| 207 | + |
| 208 | + |
| 209 | +def _interactive_repl(catalog, output_format): |
| 210 | + """Run an interactive SQL REPL.""" |
| 211 | + _setup_readline() |
| 212 | + print(_PAIMON_BANNER) |
| 213 | + |
| 214 | + try: |
| 215 | + while True: |
| 216 | + query = _read_multiline_query() |
| 217 | + if query is None: |
| 218 | + print("\nBye!") |
| 219 | + break |
| 220 | + |
| 221 | + if not query: |
| 222 | + continue |
| 223 | + |
| 224 | + lower = query.lower().rstrip(';').strip() |
| 225 | + if lower in ('exit', 'quit'): |
| 226 | + print("Bye!") |
| 227 | + break |
| 228 | + if lower == 'help': |
| 229 | + _print_help() |
| 230 | + continue |
| 231 | + |
| 232 | + # Handle USE <database> |
| 233 | + use_match = _USE_PATTERN.match(query) |
| 234 | + if use_match: |
| 235 | + _handle_use(catalog, use_match) |
| 236 | + continue |
| 237 | + |
| 238 | + # Handle SHOW DATABASES |
| 239 | + if _SHOW_DATABASES_PATTERN.match(query): |
| 240 | + _handle_show_databases(catalog) |
| 241 | + print() |
| 242 | + continue |
| 243 | + |
| 244 | + # Handle SHOW TABLES |
| 245 | + if _SHOW_TABLES_PATTERN.match(query): |
| 246 | + _handle_show_tables(catalog) |
| 247 | + print() |
| 248 | + continue |
| 249 | + |
| 250 | + try: |
| 251 | + start = time.time() |
| 252 | + result = catalog.sql(query) |
| 253 | + elapsed = time.time() - start |
| 254 | + _print_result(result, output_format, elapsed) |
| 255 | + print() |
| 256 | + except Exception as e: |
| 257 | + print(f"Error: {e}\n", file=sys.stderr) |
| 258 | + finally: |
| 259 | + _save_history() |
| 260 | + |
| 261 | + |
| 262 | +def _print_help(): |
| 263 | + """Print REPL help information.""" |
| 264 | + print(""" |
| 265 | +Commands: |
| 266 | + USE <database>; Switch the default database |
| 267 | + SHOW DATABASES; List all databases |
| 268 | + SHOW TABLES; List tables in the current database |
| 269 | + SELECT ... FROM <table>; Execute a SQL query |
| 270 | + exit / quit Exit the REPL |
| 271 | +
|
| 272 | +Table reference: |
| 273 | + <table> Table in the current default database |
| 274 | + <database>.<table> Table in a specific database |
| 275 | +
|
| 276 | +Tips: |
| 277 | + - SQL statements end with ';' and can span multiple lines |
| 278 | + - Arrow keys are supported for line editing and command history |
| 279 | + - Command history is saved across sessions (~/.paimon_history) |
| 280 | +""") |
99 | 281 |
|
100 | 282 |
|
101 | 283 | def add_sql_subcommand(subparsers): |
|
0 commit comments