Skip to content

Commit 5bc50bf

Browse files
committed
enhance
1 parent 5f6c7a4 commit 5bc50bf

File tree

3 files changed

+249
-33
lines changed

3 files changed

+249
-33
lines changed

docs/content/pypaimon/cli.md

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -670,27 +670,56 @@ paimon sql "SELECT * FROM users LIMIT 5" --format json
670670

671671
### Interactive REPL
672672

673-
Start an interactive SQL session by running `paimon sql` without a query argument:
673+
Start an interactive SQL session by running `paimon sql` without a query argument. The REPL supports arrow keys for line editing, and command history is persisted across sessions in `~/.paimon_history`.
674674

675675
```shell
676676
paimon sql
677677
```
678678

679679
Output:
680680
```
681-
Paimon SQL (powered by pypaimon-rust + DataFusion)
682-
Type your SQL queries. Use Ctrl+D or 'exit' to quit.
681+
____ _
682+
/ __ \____ _(_)___ ___ ____ ____
683+
/ /_/ / __ `/ / __ `__ \/ __ \/ __ \
684+
/ ____/ /_/ / / / / / / / /_/ / / / /
685+
/_/ \__,_/_/_/ /_/ /_/\____/_/ /_/
683686
684-
paimon> SELECT count(*) AS cnt FROM users
685-
cnt
686-
5
687+
Powered by pypaimon-rust + DataFusion
688+
Type 'help' for usage, 'exit' to quit.
687689
688-
paimon> SELECT * FROM users WHERE age > 25 ORDER BY age
689-
id name age city
690-
2 Bob 30 Shanghai
691-
3 Charlie 35 Guangzhou
690+
paimon> SHOW DATABASES;
691+
default
692+
mydb
693+
694+
paimon> USE mydb;
695+
Using database 'mydb'.
696+
697+
paimon> SHOW TABLES;
698+
orders
699+
users
700+
701+
paimon> SELECT count(*) AS cnt
702+
> FROM users
703+
> WHERE age > 18;
704+
cnt
705+
42
706+
(1 row in 0.05s)
692707
693708
paimon> exit
709+
Bye!
694710
```
695711

712+
SQL statements end with `;` and can span multiple lines. The continuation prompt ` >` indicates that more input is expected.
713+
714+
**REPL Commands:**
715+
716+
| Command | Description |
717+
|---|---|
718+
| `USE <database>;` | Switch the default database |
719+
| `SHOW DATABASES;` | List all databases |
720+
| `SHOW TABLES;` | List tables in the current database |
721+
| `SELECT ...;` | Execute a SQL query |
722+
| `help` | Show usage information |
723+
| `exit` / `quit` | Exit the REPL |
724+
696725
For more details on SQL syntax and the Python API, see [SQL Query]({{< ref "pypaimon/sql" >}}).

paimon-python/pypaimon/cli/cli_sql.py

Lines changed: 205 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,62 @@
2121
This module provides SQL query capability via pypaimon-rust + DataFusion.
2222
"""
2323

24+
import os
25+
import re
2426
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
2580

2681

2782
def cmd_sql(args):
@@ -59,43 +114,170 @@ def _execute_query(catalog, query, output_format):
59114
print(f"Error: {e}", file=sys.stderr)
60115
sys.exit(1)
61116

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()
62123
if output_format == 'json':
63124
import json
64-
df = result.to_pandas()
65125
print(json.dumps(df.to_dict(orient='records'), ensure_ascii=False))
66126
else:
67-
df = result.to_pandas()
68127
print(df.to_string(index=False))
69128

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'})")
70134

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")
75135

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
76143
while True:
77144
try:
78-
query = input("paimon> ").strip()
145+
line = input(prompt)
79146
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()
82155

83-
if not query:
156+
if not joined:
157+
lines.clear()
158+
prompt = _PROMPT
84159
continue
85-
if query.lower() in ('exit', 'quit'):
86-
break
87160

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+
""")
99281

100282

101283
def add_sql_subcommand(subparsers):

paimon-python/pypaimon/sql/sql_context.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ def sql(self, query: str) -> pa.Table:
7070
batches = df.collect()
7171
return pa.Table.from_batches(batches)
7272

73+
def use_database(self, database: str):
74+
"""Switch the default database."""
75+
self._default_database = database
76+
self._ctx.sql(f"SET datafusion.catalog.default_schema = '{database}'")
77+
7378
def sql_to_pandas(self, query: str):
7479
"""Execute a SQL query and return the result as a Pandas DataFrame."""
7580
return self.sql(query).to_pandas()

0 commit comments

Comments
 (0)