qql-cli 1.2.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qql-cli
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: A SQL-like query language CLI wrapper for Qdrant vector database
5
5
  Project-URL: Homepage, https://github.com/pavanjava/qql
6
6
  Project-URL: Repository, https://github.com/pavanjava/qql
@@ -83,6 +83,7 @@ qql> SEARCH notes SIMILAR TO 'vector databases' LIMIT 5 USING HYBRID RERANK
83
83
  - [The QQL Shell](#the-qql-shell)
84
84
  - [All QQL Operations](#all-qql-operations)
85
85
  - [INSERT — add a point](#insert--add-a-point)
86
+ - [INSERT BULK — batch insert](#insert-bulk--batch-insert-multiple-points)
86
87
  - [SEARCH — find similar points](#search--find-similar-points)
87
88
  - [Query-Time Search Params (`EXACT`, `WITH`)](#query-time-search-params-exact-with)
88
89
  - [WHERE Clause Filters](#where-clause-filters)
@@ -92,6 +93,9 @@ qql> SEARCH notes SIMILAR TO 'vector databases' LIMIT 5 USING HYBRID RERANK
92
93
  - [CREATE COLLECTION — create a collection](#create-collection--create-a-collection)
93
94
  - [DROP COLLECTION — delete a collection](#drop-collection--delete-a-collection)
94
95
  - [DELETE — remove a point](#delete--remove-a-point)
96
+ - [Script Files](#script-files)
97
+ - [EXECUTE — run a script file](#execute--run-a-qql-script-file)
98
+ - [DUMP COLLECTION — export to script](#dump-collection--export-collection-to-a-qql-script-file)
95
99
  - [Embedding Models](#embedding-models)
96
100
  - [Value Types in Dictionaries](#value-types-in-dictionaries)
97
101
  - [Configuration File](#configuration-file)
@@ -886,6 +890,158 @@ To find a point's ID, run a SEARCH first and copy the ID from the results table.
886
890
 
887
891
  ---
888
892
 
893
+ ## Script Files
894
+
895
+ QQL supports reading from and writing to `.qql` script files, making it easy to automate bulk operations, seed databases, and back up collections.
896
+
897
+ ---
898
+
899
+ ### EXECUTE — run a .qql script file
900
+
901
+ Execute a file containing multiple QQL statements in sequence. Each statement is parsed and executed in order. `--` comments are stripped before parsing.
902
+
903
+ **CLI usage:**
904
+ ```bash
905
+ qql execute /path/to/script.qql
906
+
907
+ # Stop on first error instead of continuing through all statements
908
+ qql execute /path/to/script.qql --stop-on-error
909
+ ```
910
+
911
+ **In-shell usage (inside the QQL REPL):**
912
+ ```
913
+ qql> EXECUTE /path/to/script.qql
914
+ qql> \e /path/to/script.qql
915
+ ```
916
+
917
+ **Script format:**
918
+
919
+ ```sql
920
+ -- This is a comment — the entire line is ignored
921
+ -- ============================================================
922
+ -- QQL Script — populate articles collection
923
+ -- ============================================================
924
+
925
+ -- Step 1: create the collection
926
+ CREATE COLLECTION articles
927
+
928
+ -- Step 2: bulk insert records
929
+ INSERT BULK INTO COLLECTION articles VALUES [
930
+ {'text': 'Neural networks learn representations', 'year': 2023},
931
+ {'text': 'Attention mechanisms in transformers', 'year': 2024}
932
+ ]
933
+
934
+ -- Step 3: verify
935
+ SHOW COLLECTIONS
936
+ ```
937
+
938
+ **Rules:**
939
+ - `--` to end-of-line is a comment and is ignored (inline or full-line)
940
+ - Statements can span multiple lines (e.g. `INSERT BULK ... VALUES [...]`)
941
+ - Blank lines between statements are ignored
942
+ - By default all statements run even if one fails; use `--stop-on-error` to halt early
943
+
944
+ **Example output:**
945
+ ```
946
+ Executing: /path/to/script.qql
947
+
948
+ [1/3] CREATE COLLECTION articles
949
+ ✓ Collection 'articles' created (384-dimensional vectors, cosine distance)
950
+ [2/3] INSERT BULK INTO COLLECTION articles VALUES [ …
951
+ ✓ Inserted 2 points
952
+ [3/3] SHOW COLLECTIONS
953
+ ✓ 1 collection(s) found
954
+
955
+ Done. 3/3 statement(s) succeeded.
956
+ ```
957
+
958
+ ---
959
+
960
+ ### DUMP COLLECTION — export collection to a .qql script file
961
+
962
+ 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.
963
+
964
+ **CLI usage:**
965
+ ```bash
966
+ qql dump <collection_name> <output.qql>
967
+ ```
968
+
969
+ **In-shell usage (inside the QQL REPL):**
970
+ ```
971
+ qql> DUMP COLLECTION <name> <output.qql>
972
+ ```
973
+
974
+ **Example:**
975
+ ```bash
976
+ qql dump medical_records /tmp/medical_records.qql
977
+ ```
978
+
979
+ ```
980
+ Dumping: 'medical_records' → /tmp/medical_records.qql
981
+
982
+ Collection type : hybrid (dense + sparse)
983
+ Points : 41
984
+ Batches : 1 (50 points/batch)
985
+
986
+ [1/1] wrote 41 point(s)
987
+
988
+ Done. 41 point(s) written.
989
+ ```
990
+
991
+ **Generated file structure:**
992
+ ```sql
993
+ -- ============================================================
994
+ -- QQL Dump — collection: medical_records
995
+ -- Generated : 2026-04-19 14:32:11
996
+ -- Points : 41
997
+ -- Type : hybrid (dense + sparse)
998
+ -- Note : Re-importing re-embeds all text using the
999
+ -- configured model (see: qql connect).
1000
+ -- ============================================================
1001
+
1002
+ CREATE COLLECTION medical_records HYBRID
1003
+
1004
+ -- Batch 1 / 1 (records 1–41)
1005
+ INSERT BULK INTO COLLECTION medical_records VALUES [
1006
+ {
1007
+ 'text': 'Alzheimers disease is characterized by...',
1008
+ 'title': 'Alzheimers Disease Overview',
1009
+ 'department': 'neurology',
1010
+ 'year': 2023,
1011
+ 'peer_reviewed': true
1012
+ },
1013
+ ...
1014
+ ] USING HYBRID
1015
+
1016
+ -- ============================================================
1017
+ -- End of dump
1018
+ -- Written : 41
1019
+ -- Skipped : 0 (no 'text' field)
1020
+ -- ============================================================
1021
+ ```
1022
+
1023
+ **Round-trip workflow — backup and restore:**
1024
+ ```bash
1025
+ # 1. Dump the collection
1026
+ qql dump medical_records backup.qql
1027
+
1028
+ # 2. Drop it
1029
+ qql> DROP COLLECTION medical_records
1030
+
1031
+ # 3. Restore from the dump
1032
+ qql execute backup.qql
1033
+ ```
1034
+
1035
+ **Rules and notes:**
1036
+ - Points without a `'text'` payload field are **skipped** (counted in the footer comment).
1037
+ - Hybrid collections produce `CREATE COLLECTION <name> HYBRID` and `INSERT BULK ... USING HYBRID` statements.
1038
+ - Dense collections produce plain `CREATE COLLECTION <name>` and `INSERT BULK` statements.
1039
+ - All payload value types are preserved: strings, integers, floats, booleans (`true`/`false`), `null`, lists, and nested dicts.
1040
+ - Re-importing re-embeds all text using your currently configured model — use the same model as the original collection to preserve semantic accuracy.
1041
+ - Parent directories of the output path are created automatically.
1042
+
1043
+ ---
1044
+
889
1045
  ## Embedding Models
890
1046
 
891
1047
  QQL uses [Fastembed](https://github.com/qdrant/fastembed) to convert text into vectors locally — no external API call is needed.
@@ -35,6 +35,7 @@ qql> SEARCH notes SIMILAR TO 'vector databases' LIMIT 5 USING HYBRID RERANK
35
35
  - [The QQL Shell](#the-qql-shell)
36
36
  - [All QQL Operations](#all-qql-operations)
37
37
  - [INSERT — add a point](#insert--add-a-point)
38
+ - [INSERT BULK — batch insert](#insert-bulk--batch-insert-multiple-points)
38
39
  - [SEARCH — find similar points](#search--find-similar-points)
39
40
  - [Query-Time Search Params (`EXACT`, `WITH`)](#query-time-search-params-exact-with)
40
41
  - [WHERE Clause Filters](#where-clause-filters)
@@ -44,6 +45,9 @@ qql> SEARCH notes SIMILAR TO 'vector databases' LIMIT 5 USING HYBRID RERANK
44
45
  - [CREATE COLLECTION — create a collection](#create-collection--create-a-collection)
45
46
  - [DROP COLLECTION — delete a collection](#drop-collection--delete-a-collection)
46
47
  - [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)
47
51
  - [Embedding Models](#embedding-models)
48
52
  - [Value Types in Dictionaries](#value-types-in-dictionaries)
49
53
  - [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.
838
842
 
839
843
  ---
840
844
 
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
+
841
997
  ## Embedding Models
842
998
 
843
999
  QQL uses [Fastembed](https://github.com/qdrant/fastembed) to convert text into vectors locally — no external API call is needed.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qql-cli"
3
- version = "1.2.0"
3
+ version = "1.3.0"
4
4
  description = "A SQL-like query language CLI wrapper for Qdrant vector database"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -56,6 +56,15 @@ Available statements:
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