Skip to content

Commit 0b1c273

Browse files
committed
working with script files is here
1 parent b907185 commit 0b1c273

6 files changed

Lines changed: 1097 additions & 0 deletions

File tree

README.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ qql> SEARCH notes SIMILAR TO 'vector databases' LIMIT 5 USING HYBRID RERANK
3535
- [The QQL Shell](#the-qql-shell)
3636
- [All QQL Operations](#all-qql-operations)
3737
- [INSERT — add a point](#insert--add-a-point)
38+
- [INSERT BULK — batch insert](#insert-bulk--batch-insert-multiple-points)
3839
- [SEARCH — find similar points](#search--find-similar-points)
3940
- [Query-Time Search Params (`EXACT`, `WITH`)](#query-time-search-params-exact-with)
4041
- [WHERE Clause Filters](#where-clause-filters)
@@ -44,6 +45,9 @@ qql> SEARCH notes SIMILAR TO 'vector databases' LIMIT 5 USING HYBRID RERANK
4445
- [CREATE COLLECTION — create a collection](#create-collection--create-a-collection)
4546
- [DROP COLLECTION — delete a collection](#drop-collection--delete-a-collection)
4647
- [DELETE — remove a point](#delete--remove-a-point)
48+
- [Script Files](#script-files)
49+
- [EXECUTE — run a script file](#execute--run-a-qql-script-file)
50+
- [DUMP COLLECTION — export to script](#dump-collection--export-collection-to-a-qql-script-file)
4751
- [Embedding Models](#embedding-models)
4852
- [Value Types in Dictionaries](#value-types-in-dictionaries)
4953
- [Configuration File](#configuration-file)
@@ -838,6 +842,158 @@ To find a point's ID, run a SEARCH first and copy the ID from the results table.
838842

839843
---
840844

845+
## Script Files
846+
847+
QQL supports reading from and writing to `.qql` script files, making it easy to automate bulk operations, seed databases, and back up collections.
848+
849+
---
850+
851+
### EXECUTE — run a .qql script file
852+
853+
Execute a file containing multiple QQL statements in sequence. Each statement is parsed and executed in order. `--` comments are stripped before parsing.
854+
855+
**CLI usage:**
856+
```bash
857+
qql execute /path/to/script.qql
858+
859+
# Stop on first error instead of continuing through all statements
860+
qql execute /path/to/script.qql --stop-on-error
861+
```
862+
863+
**In-shell usage (inside the QQL REPL):**
864+
```
865+
qql> EXECUTE /path/to/script.qql
866+
qql> \e /path/to/script.qql
867+
```
868+
869+
**Script format:**
870+
871+
```sql
872+
-- This is a comment — the entire line is ignored
873+
-- ============================================================
874+
-- QQL Script — populate articles collection
875+
-- ============================================================
876+
877+
-- Step 1: create the collection
878+
CREATE COLLECTION articles
879+
880+
-- Step 2: bulk insert records
881+
INSERT BULK INTO COLLECTION articles VALUES [
882+
{'text': 'Neural networks learn representations', 'year': 2023},
883+
{'text': 'Attention mechanisms in transformers', 'year': 2024}
884+
]
885+
886+
-- Step 3: verify
887+
SHOW COLLECTIONS
888+
```
889+
890+
**Rules:**
891+
- `--` to end-of-line is a comment and is ignored (inline or full-line)
892+
- Statements can span multiple lines (e.g. `INSERT BULK ... VALUES [...]`)
893+
- Blank lines between statements are ignored
894+
- By default all statements run even if one fails; use `--stop-on-error` to halt early
895+
896+
**Example output:**
897+
```
898+
Executing: /path/to/script.qql
899+
900+
[1/3] CREATE COLLECTION articles
901+
✓ Collection 'articles' created (384-dimensional vectors, cosine distance)
902+
[2/3] INSERT BULK INTO COLLECTION articles VALUES [ …
903+
✓ Inserted 2 points
904+
[3/3] SHOW COLLECTIONS
905+
✓ 1 collection(s) found
906+
907+
Done. 3/3 statement(s) succeeded.
908+
```
909+
910+
---
911+
912+
### DUMP COLLECTION — export collection to a .qql script file
913+
914+
Export every point in a collection to a `.qql` script file. The generated file is valid QQL — it can be re-imported with `qql execute` to restore or migrate the collection. Points are written in batches of 50 as `INSERT BULK` statements.
915+
916+
**CLI usage:**
917+
```bash
918+
qql dump <collection_name> <output.qql>
919+
```
920+
921+
**In-shell usage (inside the QQL REPL):**
922+
```
923+
qql> DUMP COLLECTION <name> <output.qql>
924+
```
925+
926+
**Example:**
927+
```bash
928+
qql dump medical_records /tmp/medical_records.qql
929+
```
930+
931+
```
932+
Dumping: 'medical_records' → /tmp/medical_records.qql
933+
934+
Collection type : hybrid (dense + sparse)
935+
Points : 41
936+
Batches : 1 (50 points/batch)
937+
938+
[1/1] wrote 41 point(s)
939+
940+
Done. 41 point(s) written.
941+
```
942+
943+
**Generated file structure:**
944+
```sql
945+
-- ============================================================
946+
-- QQL Dump — collection: medical_records
947+
-- Generated : 2026-04-19 14:32:11
948+
-- Points : 41
949+
-- Type : hybrid (dense + sparse)
950+
-- Note : Re-importing re-embeds all text using the
951+
-- configured model (see: qql connect).
952+
-- ============================================================
953+
954+
CREATE COLLECTION medical_records HYBRID
955+
956+
-- Batch 1 / 1 (records 1–41)
957+
INSERT BULK INTO COLLECTION medical_records VALUES [
958+
{
959+
'text': 'Alzheimers disease is characterized by...',
960+
'title': 'Alzheimers Disease Overview',
961+
'department': 'neurology',
962+
'year': 2023,
963+
'peer_reviewed': true
964+
},
965+
...
966+
] USING HYBRID
967+
968+
-- ============================================================
969+
-- End of dump
970+
-- Written : 41
971+
-- Skipped : 0 (no 'text' field)
972+
-- ============================================================
973+
```
974+
975+
**Round-trip workflow — backup and restore:**
976+
```bash
977+
# 1. Dump the collection
978+
qql dump medical_records backup.qql
979+
980+
# 2. Drop it
981+
qql> DROP COLLECTION medical_records
982+
983+
# 3. Restore from the dump
984+
qql execute backup.qql
985+
```
986+
987+
**Rules and notes:**
988+
- Points without a `'text'` payload field are **skipped** (counted in the footer comment).
989+
- Hybrid collections produce `CREATE COLLECTION <name> HYBRID` and `INSERT BULK ... USING HYBRID` statements.
990+
- Dense collections produce plain `CREATE COLLECTION <name>` and `INSERT BULK` statements.
991+
- All payload value types are preserved: strings, integers, floats, booleans (`true`/`false`), `null`, lists, and nested dicts.
992+
- Re-importing re-embeds all text using your currently configured model — use the same model as the original collection to preserve semantic accuracy.
993+
- Parent directories of the output path are created automatically.
994+
995+
---
996+
841997
## Embedding Models
842998

843999
QQL uses [Fastembed](https://github.com/qdrant/fastembed) to convert text into vectors locally — no external API call is needed.

src/qql/cli.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,15 @@
5656
[yellow]DELETE FROM[/yellow] <name> [yellow]WHERE id =[/yellow] '<id>'
5757
Delete a point by its ID.
5858
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+
5968
Keyboard shortcuts:
6069
← → arrows move cursor within the current line
6170
↑ ↓ arrows scroll through command history
@@ -119,6 +128,109 @@ def disconnect() -> None:
119128
console.print("Disconnected. Config removed.")
120129

121130

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+
122234
# ── REPL ───────────────────────────────────────────────────────────────────────
123235

124236
def _launch_repl(cfg: QQLConfig) -> None:
@@ -161,6 +273,62 @@ def _launch_repl(cfg: QQLConfig) -> None:
161273
console.print(HELP_TEXT)
162274
continue
163275

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+
164332
_run_and_print(executor, query)
165333

166334

0 commit comments

Comments
 (0)