|
56 | 56 | [yellow]DELETE FROM[/yellow] <name> [yellow]WHERE id =[/yellow] '<id>' |
57 | 57 | Delete a point by its ID. |
58 | 58 |
|
| 59 | +Script files (in-shell): |
| 60 | + [yellow]EXECUTE[/yellow] <path> or [yellow]\\e[/yellow] <path> |
| 61 | + Run a .qql script file. Statements are executed in order. |
| 62 | + Lines starting with [yellow]--[/yellow] are treated as comments and ignored. |
| 63 | +
|
| 64 | + [yellow]DUMP[/yellow] <name> <output.qql> or [yellow]DUMP COLLECTION[/yellow] <name> <output.qql> |
| 65 | + Export all points in a collection to a .qql script file. |
| 66 | + The file can be re-imported with EXECUTE. |
| 67 | +
|
59 | 68 | Keyboard shortcuts: |
60 | 69 | ← → arrows move cursor within the current line |
61 | 70 | ↑ ↓ arrows scroll through command history |
@@ -119,6 +128,109 @@ def disconnect() -> None: |
119 | 128 | console.print("Disconnected. Config removed.") |
120 | 129 |
|
121 | 130 |
|
| 131 | +# ── execute ──────────────────────────────────────────────────────────────────── |
| 132 | + |
| 133 | +@main.command() |
| 134 | +@click.argument("file", type=click.Path(exists=True, readable=True)) |
| 135 | +@click.option( |
| 136 | + "--stop-on-error", |
| 137 | + is_flag=True, |
| 138 | + default=False, |
| 139 | + help="Halt execution on the first statement error (default: continue all).", |
| 140 | +) |
| 141 | +def execute(file: str, stop_on_error: bool) -> None: |
| 142 | + """Execute a .qql script file against the connected Qdrant instance. |
| 143 | +
|
| 144 | + Lines beginning with -- are treated as comments and skipped. |
| 145 | + Each QQL statement is executed in order and its result is printed. |
| 146 | + """ |
| 147 | + from qdrant_client import QdrantClient |
| 148 | + |
| 149 | + cfg = load_config() |
| 150 | + if cfg is None: |
| 151 | + err_console.print( |
| 152 | + "[bold red]Not connected.[/bold red] " |
| 153 | + "Run: [bold]qql connect --url <url>[/bold]" |
| 154 | + ) |
| 155 | + sys.exit(1) |
| 156 | + |
| 157 | + try: |
| 158 | + client = QdrantClient(url=cfg.url, api_key=cfg.secret) |
| 159 | + client.get_collections() |
| 160 | + except Exception as e: |
| 161 | + err_console.print(f"[bold red]Connection failed:[/bold red] {e}") |
| 162 | + sys.exit(1) |
| 163 | + |
| 164 | + from .executor import Executor |
| 165 | + from .script import run_script |
| 166 | + |
| 167 | + executor = Executor(client, cfg) |
| 168 | + console.print(f"[bold cyan]Executing:[/bold cyan] {file}\n") |
| 169 | + |
| 170 | + ok, fail = run_script(file, executor, console, err_console, stop_on_error) |
| 171 | + total = ok + fail |
| 172 | + |
| 173 | + if fail == 0: |
| 174 | + console.print( |
| 175 | + f"\n[bold green]Done.[/bold green] " |
| 176 | + f"{total}/{total} statement(s) succeeded." |
| 177 | + ) |
| 178 | + else: |
| 179 | + console.print( |
| 180 | + f"\n[bold yellow]Done.[/bold yellow] " |
| 181 | + f"{ok}/{total} succeeded, [bold red]{fail} failed[/bold red]." |
| 182 | + ) |
| 183 | + sys.exit(1) |
| 184 | + |
| 185 | + |
| 186 | +# ── dump ─────────────────────────────────────────────────────────────────────── |
| 187 | + |
| 188 | +@main.command() |
| 189 | +@click.argument("collection") |
| 190 | +@click.argument("output", type=click.Path()) |
| 191 | +def dump(collection: str, output: str) -> None: |
| 192 | + """Dump a collection to a .qql script file. |
| 193 | +
|
| 194 | + OUTPUT is the path for the generated .qql file. |
| 195 | + The file contains CREATE COLLECTION + INSERT BULK statements and can be |
| 196 | + re-imported with: qql execute <output> |
| 197 | + """ |
| 198 | + from qdrant_client import QdrantClient |
| 199 | + |
| 200 | + cfg = load_config() |
| 201 | + if cfg is None: |
| 202 | + err_console.print( |
| 203 | + "[bold red]Not connected.[/bold red] " |
| 204 | + "Run: [bold]qql connect --url <url>[/bold]" |
| 205 | + ) |
| 206 | + sys.exit(1) |
| 207 | + |
| 208 | + try: |
| 209 | + client = QdrantClient(url=cfg.url, api_key=cfg.secret) |
| 210 | + client.get_collections() |
| 211 | + except Exception as e: |
| 212 | + err_console.print(f"[bold red]Connection failed:[/bold red] {e}") |
| 213 | + sys.exit(1) |
| 214 | + |
| 215 | + from .dumper import dump_collection |
| 216 | + |
| 217 | + console.print( |
| 218 | + f"[bold cyan]Dumping:[/bold cyan] '{collection}' → {output}\n" |
| 219 | + ) |
| 220 | + written, skipped = dump_collection(collection, output, client, console, err_console) |
| 221 | + |
| 222 | + if written == 0 and skipped == 0: |
| 223 | + # collection not found — error already printed by dump_collection |
| 224 | + sys.exit(1) |
| 225 | + |
| 226 | + console.print( |
| 227 | + f"\n[bold green]Done.[/bold green] " |
| 228 | + f"{written} point(s) written" |
| 229 | + + (f", [yellow]{skipped} skipped[/yellow] (no 'text' field)" if skipped else "") |
| 230 | + + f"." |
| 231 | + ) |
| 232 | + |
| 233 | + |
122 | 234 | # ── REPL ─────────────────────────────────────────────────────────────────────── |
123 | 235 |
|
124 | 236 | def _launch_repl(cfg: QQLConfig) -> None: |
@@ -161,6 +273,62 @@ def _launch_repl(cfg: QQLConfig) -> None: |
161 | 273 | console.print(HELP_TEXT) |
162 | 274 | continue |
163 | 275 |
|
| 276 | + # ── EXECUTE <path> / \e <path> — run a .qql script file ────────── |
| 277 | + if low.startswith("execute ") or low.startswith("\\e "): |
| 278 | + script_path = query.split(None, 1)[1].strip() |
| 279 | + from .script import run_script |
| 280 | + ok, fail = run_script(script_path, executor, console, err_console) |
| 281 | + total = ok + fail |
| 282 | + if fail == 0: |
| 283 | + console.print( |
| 284 | + f"[bold green]Done.[/bold green] " |
| 285 | + f"{total}/{total} statement(s) succeeded." |
| 286 | + ) |
| 287 | + else: |
| 288 | + console.print( |
| 289 | + f"[bold yellow]Done.[/bold yellow] " |
| 290 | + f"{ok}/{total} succeeded, [bold red]{fail} failed[/bold red]." |
| 291 | + ) |
| 292 | + continue |
| 293 | + |
| 294 | + # ── DUMP [COLLECTION] <name> <file> — export collection to .qql ── |
| 295 | + # Accepts both: |
| 296 | + # DUMP COLLECTION <name> <output.qql> |
| 297 | + # DUMP <name> <output.qql> |
| 298 | + if low.startswith("dump "): |
| 299 | + parts = query.split(None, 3) # up to 4 tokens |
| 300 | + if len(parts) >= 2 and parts[1].lower() == "collection": |
| 301 | + # DUMP COLLECTION <name> <file> |
| 302 | + if len(parts) < 4: |
| 303 | + err_console.print( |
| 304 | + "[bold red]Usage:[/bold red] DUMP COLLECTION <name> <output.qql>" |
| 305 | + ) |
| 306 | + continue |
| 307 | + coll_name, out_path = parts[2], parts[3] |
| 308 | + else: |
| 309 | + # DUMP <name> <file> |
| 310 | + if len(parts) < 3: |
| 311 | + err_console.print( |
| 312 | + "[bold red]Usage:[/bold red] DUMP <name> <output.qql>" |
| 313 | + ) |
| 314 | + continue |
| 315 | + coll_name, out_path = parts[1], parts[2] |
| 316 | + from .dumper import dump_collection |
| 317 | + console.print( |
| 318 | + f"[bold cyan]Dumping:[/bold cyan] '{coll_name}' → {out_path}\n" |
| 319 | + ) |
| 320 | + written, skipped = dump_collection( |
| 321 | + coll_name, out_path, client, console, err_console |
| 322 | + ) |
| 323 | + if written > 0 or skipped == 0: |
| 324 | + console.print( |
| 325 | + f"[bold green]Done.[/bold green] " |
| 326 | + f"{written} point(s) written" |
| 327 | + + (f", [yellow]{skipped} skipped[/yellow] (no 'text' field)" if skipped else "") |
| 328 | + + "." |
| 329 | + ) |
| 330 | + continue |
| 331 | + |
164 | 332 | _run_and_print(executor, query) |
165 | 333 |
|
166 | 334 |
|
|
0 commit comments