jmd-mcp-sql 0.4__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.
- jmd_mcp_sql-0.4/LICENSE +21 -0
- jmd_mcp_sql-0.4/PKG-INFO +345 -0
- jmd_mcp_sql-0.4/README.md +308 -0
- jmd_mcp_sql-0.4/jmd_mcp_sql/__init__.py +4 -0
- jmd_mcp_sql-0.4/jmd_mcp_sql/schema.py +125 -0
- jmd_mcp_sql-0.4/jmd_mcp_sql/server.py +355 -0
- jmd_mcp_sql-0.4/jmd_mcp_sql/translator.py +1625 -0
- jmd_mcp_sql-0.4/jmd_mcp_sql.egg-info/PKG-INFO +345 -0
- jmd_mcp_sql-0.4/jmd_mcp_sql.egg-info/SOURCES.txt +14 -0
- jmd_mcp_sql-0.4/jmd_mcp_sql.egg-info/dependency_links.txt +1 -0
- jmd_mcp_sql-0.4/jmd_mcp_sql.egg-info/entry_points.txt +2 -0
- jmd_mcp_sql-0.4/jmd_mcp_sql.egg-info/requires.txt +2 -0
- jmd_mcp_sql-0.4/jmd_mcp_sql.egg-info/top_level.txt +1 -0
- jmd_mcp_sql-0.4/pyproject.toml +58 -0
- jmd_mcp_sql-0.4/setup.cfg +4 -0
- jmd_mcp_sql-0.4/tests/test_translator.py +743 -0
jmd_mcp_sql-0.4/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andreas Ostermeyer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
jmd_mcp_sql-0.4/PKG-INFO
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jmd-mcp-sql
|
|
3
|
+
Version: 0.4
|
|
4
|
+
Summary: JMD-based MCP server for SQLite — natural language database interface
|
|
5
|
+
Author-email: Andreas Ostermeyer <andreas@ostermeyer.de>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Andreas Ostermeyer
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/ostermeyer/jmd-spec
|
|
29
|
+
Project-URL: Repository, https://github.com/ostermeyer/jmd-mcp-sql
|
|
30
|
+
Keywords: jmd,mcp,sqlite,llm,natural-language,database
|
|
31
|
+
Requires-Python: >=3.10
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
License-File: LICENSE
|
|
34
|
+
Requires-Dist: mcp>=1.0
|
|
35
|
+
Requires-Dist: jmd-format>=0.4.1
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# jmd-mcp-sql
|
|
39
|
+
|
|
40
|
+
MCP server that exposes a SQLite database through three JMD tools — a natural language database interface for LLM-driven workflows.
|
|
41
|
+
|
|
42
|
+
## Tools
|
|
43
|
+
|
|
44
|
+
| Tool | `#` Data | `#?` Query | `#!` Schema | `#-` Delete |
|
|
45
|
+
| --- | --- | --- | --- | --- |
|
|
46
|
+
| `read` | SELECT by fields | SELECT with filters + aggregation | PRAGMA (describe table) | — |
|
|
47
|
+
| `write` | INSERT OR REPLACE | — | CREATE / ALTER TABLE | — |
|
|
48
|
+
| `delete` | — | — | DROP TABLE | DELETE WHERE |
|
|
49
|
+
|
|
50
|
+
All inputs and outputs are JMD documents. The LLM speaks JMD — no SQL required.
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
Install the latest version directly from GitHub:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install git+https://github.com/ostermeyer/jmd-mcp-sql.git
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or pin a specific release:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install git+https://github.com/ostermeyer/jmd-mcp-sql.git@v0.1
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Pre-built packages are attached to each
|
|
67
|
+
[GitHub Release](https://github.com/ostermeyer/jmd-mcp-sql/releases).
|
|
68
|
+
|
|
69
|
+
## Configuration
|
|
70
|
+
|
|
71
|
+
### With Claude Code
|
|
72
|
+
|
|
73
|
+
Add to your MCP configuration (`~/.claude/settings.json`):
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"sql": {
|
|
79
|
+
"command": "jmd-mcp-sql",
|
|
80
|
+
"args": ["/path/to/your.db"]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or use the bundled Northwind demo database (no argument needed):
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"mcpServers": {
|
|
91
|
+
"sql": {
|
|
92
|
+
"command": "jmd-mcp-sql"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The demo database ships as `northwind.sql` (plain text, version-controlled). On the
|
|
99
|
+
first run without an explicit path, the server creates `northwind.db` from that file
|
|
100
|
+
automatically. The `.db` file is not tracked by git.
|
|
101
|
+
|
|
102
|
+
## JMD Document Syntax
|
|
103
|
+
|
|
104
|
+
Every document starts with a heading line that sets the document type and table name,
|
|
105
|
+
followed by `key: value` pairs (one per line):
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
# Product → data document (exact lookup / insert-or-replace)
|
|
109
|
+
#? Product → query document (filter / list / aggregate)
|
|
110
|
+
#! Product → schema document (describe / create / drop table)
|
|
111
|
+
#- Product → delete document (delete matching records)
|
|
112
|
+
|
|
113
|
+
key: value → string, integer, or float — inferred automatically
|
|
114
|
+
key: true/false → boolean
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Discovering the Database
|
|
118
|
+
|
|
119
|
+
To see which tables exist, read each table's schema:
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
read("#! Customers")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
This returns a `#!` document with column names, JMD types, and modifiers
|
|
126
|
+
(`readonly` = primary key, `optional` = nullable).
|
|
127
|
+
|
|
128
|
+
## Typical Workflows
|
|
129
|
+
|
|
130
|
+
**List all rows** (small tables only):
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
read("#? Orders")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Filter rows — equality:**
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
read("#? Orders\nstatus: shipped")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Filter rows — comparison:**
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
read("#? Orders\nFreight: > 50")
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Filter rows — alternation (OR):**
|
|
149
|
+
|
|
150
|
+
```text
|
|
151
|
+
read("#? Orders\nShipCountry: Germany|France|UK")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Filter rows — contains (case-insensitive substring):**
|
|
155
|
+
|
|
156
|
+
```text
|
|
157
|
+
read("#? Customers\nCompanyName: ~Corp")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Filter rows — regex pattern:**
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
read("#? Products\nProductName: ^Chai.*")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Filter rows — negation** (composes with any operator):
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
read("#? Orders\nShipCountry: !Germany")
|
|
170
|
+
read("#? Products\nProductName: !^LEGACY.*")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Look up one record:**
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
read("# Customers\nid: 42")
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Insert or replace a record:**
|
|
180
|
+
|
|
181
|
+
```text
|
|
182
|
+
write("# Orders\nid: 1\nstatus: pending\ntotal: 99.90")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Create a table:**
|
|
186
|
+
|
|
187
|
+
```text
|
|
188
|
+
write("#! Products\nid: integer readonly\nname: string\nprice: float optional")
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Delete a record:**
|
|
192
|
+
|
|
193
|
+
```text
|
|
194
|
+
delete("#- Orders\nid: 1")
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Drop a table:**
|
|
198
|
+
|
|
199
|
+
```text
|
|
200
|
+
delete("#! OldTable")
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Pagination
|
|
204
|
+
|
|
205
|
+
Always use pagination when querying tables that may contain many rows.
|
|
206
|
+
|
|
207
|
+
Use frontmatter fields before the `#?` heading to control pagination:
|
|
208
|
+
|
|
209
|
+
```text
|
|
210
|
+
read("page-size: 50\npage: 1\n\n#? Orders")
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The response carries pagination metadata as **frontmatter** — before the root heading:
|
|
214
|
+
|
|
215
|
+
```text
|
|
216
|
+
total: 830
|
|
217
|
+
page: 1
|
|
218
|
+
pages: 17
|
|
219
|
+
page-size: 50
|
|
220
|
+
|
|
221
|
+
# Orders
|
|
222
|
+
## data[]
|
|
223
|
+
- OrderID: 10248
|
|
224
|
+
...
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Count only** (no rows returned):
|
|
228
|
+
|
|
229
|
+
```text
|
|
230
|
+
read("count: true\n\n#? Orders")
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
|
|
235
|
+
```text
|
|
236
|
+
count: 830
|
|
237
|
+
|
|
238
|
+
# Orders
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Use `total` and `pages` to determine whether to fetch more pages.
|
|
242
|
+
For tables with fewer than ~20 rows pagination is optional.
|
|
243
|
+
|
|
244
|
+
## Field Projection
|
|
245
|
+
|
|
246
|
+
Use `select:` frontmatter to return only specific columns. This keeps
|
|
247
|
+
responses small and context windows focused.
|
|
248
|
+
|
|
249
|
+
```text
|
|
250
|
+
read("select: OrderID, EmployeeID\npage-size: 50\n\n#? Orders")
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Works with both `#` (data) and `#?` (query) documents. When combined with
|
|
254
|
+
aggregation, `select:` filters the result columns after the GROUP BY.
|
|
255
|
+
|
|
256
|
+
## Joins
|
|
257
|
+
|
|
258
|
+
Use `join:` frontmatter to query across multiple tables in one call.
|
|
259
|
+
The value is `<TableName> on <JoinColumn>` (INNER JOIN, equi-join on a
|
|
260
|
+
column that exists in both tables).
|
|
261
|
+
|
|
262
|
+
```text
|
|
263
|
+
read("join: Order Details on OrderID\nsum: UnitPrice * Quantity * (1 - Discount) as revenue\ngroup: EmployeeID\nsort: revenue desc\n\n#? Orders")
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Multiple joins** — comma-separated in a single `join:` value:
|
|
267
|
+
|
|
268
|
+
```text
|
|
269
|
+
join: Order Details on OrderID, Employees on EmployeeID
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Expression syntax** — use `<expression> as <alias>` in aggregate functions
|
|
273
|
+
to compute derived values across joined columns:
|
|
274
|
+
|
|
275
|
+
```text
|
|
276
|
+
sum: UnitPrice * Quantity * (1 - Discount) as revenue
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The alias becomes the result column name. Without `as`, the default alias
|
|
280
|
+
`<func>_<field>` applies (e.g. `sum_Freight`).
|
|
281
|
+
|
|
282
|
+
Allowed in expressions: column names, numeric literals, arithmetic operators
|
|
283
|
+
(`+`, `-`, `*`, `/`), and standard SQL functions (`SUM`, `AVG`, `ROUND`, …).
|
|
284
|
+
Subqueries and SQL keywords are not permitted.
|
|
285
|
+
|
|
286
|
+
**Projection rules for join queries:**
|
|
287
|
+
|
|
288
|
+
- Unambiguous columns (appear in exactly one table) resolve automatically.
|
|
289
|
+
- Join key columns always resolve to the main table.
|
|
290
|
+
- Columns present in multiple tables (other than join keys) require explicit
|
|
291
|
+
qualification — specify them via `select:` or filter on the unambiguous side.
|
|
292
|
+
|
|
293
|
+
## Aggregation
|
|
294
|
+
|
|
295
|
+
Aggregation is expressed as **frontmatter** before the `#?` heading.
|
|
296
|
+
QBE filter fields narrow rows *before* aggregation (SQL WHERE).
|
|
297
|
+
The `having:` key filters *after* aggregation (SQL HAVING).
|
|
298
|
+
|
|
299
|
+
| Key | SQL | Result column name |
|
|
300
|
+
| --- | --- | --- |
|
|
301
|
+
| `group: f1, f2` | GROUP BY | grouping keys pass through unchanged |
|
|
302
|
+
| `sum: field` | SUM(field) | `sum_field` |
|
|
303
|
+
| `avg: field` | AVG(field) | `avg_field` |
|
|
304
|
+
| `min: field` | MIN(field) | `min_field` |
|
|
305
|
+
| `max: field` | MAX(field) | `max_field` |
|
|
306
|
+
| `count` | COUNT(*) | `count` |
|
|
307
|
+
|
|
308
|
+
Multiple fields per function: `sum: Freight, Total` → `sum_Freight` and `sum_Total`.
|
|
309
|
+
|
|
310
|
+
| Frontmatter | Meaning |
|
|
311
|
+
| --- | --- |
|
|
312
|
+
| `sort: sum_revenue desc, EmployeeID asc` | ORDER BY (multiple columns, mixed) |
|
|
313
|
+
| `having: count > 5` | HAVING COUNT(*) > 5 |
|
|
314
|
+
| `having: sum_Freight > 1000, count > 2` | HAVING … AND … (comma = AND) |
|
|
315
|
+
|
|
316
|
+
`having:` supports: `>`, `>=`, `<`, `<=`, `=`.
|
|
317
|
+
`sort:` references any result column — grouping keys or aggregate aliases.
|
|
318
|
+
`page-size:` and `page:` apply to the aggregated result set.
|
|
319
|
+
|
|
320
|
+
**Example — top 3 employees by revenue:**
|
|
321
|
+
|
|
322
|
+
```text
|
|
323
|
+
read("group: EmployeeID\nsum: revenue\nsort: sum_revenue desc\npage-size: 3\n\n#? OrderDetails")
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Error Handling
|
|
327
|
+
|
|
328
|
+
All tools return a `# Error` document on failure:
|
|
329
|
+
|
|
330
|
+
```text
|
|
331
|
+
# Error
|
|
332
|
+
status: 400
|
|
333
|
+
code: not_found
|
|
334
|
+
message: No records found in Orders
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Check the `code` field to decide how to proceed.
|
|
338
|
+
|
|
339
|
+
## Specification
|
|
340
|
+
|
|
341
|
+
The JMD format is documented at [jmd-spec](https://github.com/ostermeyer/jmd-spec).
|
|
342
|
+
|
|
343
|
+
## License
|
|
344
|
+
|
|
345
|
+
MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# jmd-mcp-sql
|
|
2
|
+
|
|
3
|
+
MCP server that exposes a SQLite database through three JMD tools — a natural language database interface for LLM-driven workflows.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
| Tool | `#` Data | `#?` Query | `#!` Schema | `#-` Delete |
|
|
8
|
+
| --- | --- | --- | --- | --- |
|
|
9
|
+
| `read` | SELECT by fields | SELECT with filters + aggregation | PRAGMA (describe table) | — |
|
|
10
|
+
| `write` | INSERT OR REPLACE | — | CREATE / ALTER TABLE | — |
|
|
11
|
+
| `delete` | — | — | DROP TABLE | DELETE WHERE |
|
|
12
|
+
|
|
13
|
+
All inputs and outputs are JMD documents. The LLM speaks JMD — no SQL required.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Install the latest version directly from GitHub:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install git+https://github.com/ostermeyer/jmd-mcp-sql.git
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or pin a specific release:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install git+https://github.com/ostermeyer/jmd-mcp-sql.git@v0.1
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Pre-built packages are attached to each
|
|
30
|
+
[GitHub Release](https://github.com/ostermeyer/jmd-mcp-sql/releases).
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
### With Claude Code
|
|
35
|
+
|
|
36
|
+
Add to your MCP configuration (`~/.claude/settings.json`):
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"sql": {
|
|
42
|
+
"command": "jmd-mcp-sql",
|
|
43
|
+
"args": ["/path/to/your.db"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or use the bundled Northwind demo database (no argument needed):
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"sql": {
|
|
55
|
+
"command": "jmd-mcp-sql"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The demo database ships as `northwind.sql` (plain text, version-controlled). On the
|
|
62
|
+
first run without an explicit path, the server creates `northwind.db` from that file
|
|
63
|
+
automatically. The `.db` file is not tracked by git.
|
|
64
|
+
|
|
65
|
+
## JMD Document Syntax
|
|
66
|
+
|
|
67
|
+
Every document starts with a heading line that sets the document type and table name,
|
|
68
|
+
followed by `key: value` pairs (one per line):
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
# Product → data document (exact lookup / insert-or-replace)
|
|
72
|
+
#? Product → query document (filter / list / aggregate)
|
|
73
|
+
#! Product → schema document (describe / create / drop table)
|
|
74
|
+
#- Product → delete document (delete matching records)
|
|
75
|
+
|
|
76
|
+
key: value → string, integer, or float — inferred automatically
|
|
77
|
+
key: true/false → boolean
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Discovering the Database
|
|
81
|
+
|
|
82
|
+
To see which tables exist, read each table's schema:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
read("#! Customers")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This returns a `#!` document with column names, JMD types, and modifiers
|
|
89
|
+
(`readonly` = primary key, `optional` = nullable).
|
|
90
|
+
|
|
91
|
+
## Typical Workflows
|
|
92
|
+
|
|
93
|
+
**List all rows** (small tables only):
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
read("#? Orders")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Filter rows — equality:**
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
read("#? Orders\nstatus: shipped")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Filter rows — comparison:**
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
read("#? Orders\nFreight: > 50")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Filter rows — alternation (OR):**
|
|
112
|
+
|
|
113
|
+
```text
|
|
114
|
+
read("#? Orders\nShipCountry: Germany|France|UK")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Filter rows — contains (case-insensitive substring):**
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
read("#? Customers\nCompanyName: ~Corp")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Filter rows — regex pattern:**
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
read("#? Products\nProductName: ^Chai.*")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Filter rows — negation** (composes with any operator):
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
read("#? Orders\nShipCountry: !Germany")
|
|
133
|
+
read("#? Products\nProductName: !^LEGACY.*")
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Look up one record:**
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
read("# Customers\nid: 42")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Insert or replace a record:**
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
write("# Orders\nid: 1\nstatus: pending\ntotal: 99.90")
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Create a table:**
|
|
149
|
+
|
|
150
|
+
```text
|
|
151
|
+
write("#! Products\nid: integer readonly\nname: string\nprice: float optional")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Delete a record:**
|
|
155
|
+
|
|
156
|
+
```text
|
|
157
|
+
delete("#- Orders\nid: 1")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Drop a table:**
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
delete("#! OldTable")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Pagination
|
|
167
|
+
|
|
168
|
+
Always use pagination when querying tables that may contain many rows.
|
|
169
|
+
|
|
170
|
+
Use frontmatter fields before the `#?` heading to control pagination:
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
read("page-size: 50\npage: 1\n\n#? Orders")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The response carries pagination metadata as **frontmatter** — before the root heading:
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
total: 830
|
|
180
|
+
page: 1
|
|
181
|
+
pages: 17
|
|
182
|
+
page-size: 50
|
|
183
|
+
|
|
184
|
+
# Orders
|
|
185
|
+
## data[]
|
|
186
|
+
- OrderID: 10248
|
|
187
|
+
...
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Count only** (no rows returned):
|
|
191
|
+
|
|
192
|
+
```text
|
|
193
|
+
read("count: true\n\n#? Orders")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
|
|
198
|
+
```text
|
|
199
|
+
count: 830
|
|
200
|
+
|
|
201
|
+
# Orders
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Use `total` and `pages` to determine whether to fetch more pages.
|
|
205
|
+
For tables with fewer than ~20 rows pagination is optional.
|
|
206
|
+
|
|
207
|
+
## Field Projection
|
|
208
|
+
|
|
209
|
+
Use `select:` frontmatter to return only specific columns. This keeps
|
|
210
|
+
responses small and context windows focused.
|
|
211
|
+
|
|
212
|
+
```text
|
|
213
|
+
read("select: OrderID, EmployeeID\npage-size: 50\n\n#? Orders")
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Works with both `#` (data) and `#?` (query) documents. When combined with
|
|
217
|
+
aggregation, `select:` filters the result columns after the GROUP BY.
|
|
218
|
+
|
|
219
|
+
## Joins
|
|
220
|
+
|
|
221
|
+
Use `join:` frontmatter to query across multiple tables in one call.
|
|
222
|
+
The value is `<TableName> on <JoinColumn>` (INNER JOIN, equi-join on a
|
|
223
|
+
column that exists in both tables).
|
|
224
|
+
|
|
225
|
+
```text
|
|
226
|
+
read("join: Order Details on OrderID\nsum: UnitPrice * Quantity * (1 - Discount) as revenue\ngroup: EmployeeID\nsort: revenue desc\n\n#? Orders")
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Multiple joins** — comma-separated in a single `join:` value:
|
|
230
|
+
|
|
231
|
+
```text
|
|
232
|
+
join: Order Details on OrderID, Employees on EmployeeID
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Expression syntax** — use `<expression> as <alias>` in aggregate functions
|
|
236
|
+
to compute derived values across joined columns:
|
|
237
|
+
|
|
238
|
+
```text
|
|
239
|
+
sum: UnitPrice * Quantity * (1 - Discount) as revenue
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The alias becomes the result column name. Without `as`, the default alias
|
|
243
|
+
`<func>_<field>` applies (e.g. `sum_Freight`).
|
|
244
|
+
|
|
245
|
+
Allowed in expressions: column names, numeric literals, arithmetic operators
|
|
246
|
+
(`+`, `-`, `*`, `/`), and standard SQL functions (`SUM`, `AVG`, `ROUND`, …).
|
|
247
|
+
Subqueries and SQL keywords are not permitted.
|
|
248
|
+
|
|
249
|
+
**Projection rules for join queries:**
|
|
250
|
+
|
|
251
|
+
- Unambiguous columns (appear in exactly one table) resolve automatically.
|
|
252
|
+
- Join key columns always resolve to the main table.
|
|
253
|
+
- Columns present in multiple tables (other than join keys) require explicit
|
|
254
|
+
qualification — specify them via `select:` or filter on the unambiguous side.
|
|
255
|
+
|
|
256
|
+
## Aggregation
|
|
257
|
+
|
|
258
|
+
Aggregation is expressed as **frontmatter** before the `#?` heading.
|
|
259
|
+
QBE filter fields narrow rows *before* aggregation (SQL WHERE).
|
|
260
|
+
The `having:` key filters *after* aggregation (SQL HAVING).
|
|
261
|
+
|
|
262
|
+
| Key | SQL | Result column name |
|
|
263
|
+
| --- | --- | --- |
|
|
264
|
+
| `group: f1, f2` | GROUP BY | grouping keys pass through unchanged |
|
|
265
|
+
| `sum: field` | SUM(field) | `sum_field` |
|
|
266
|
+
| `avg: field` | AVG(field) | `avg_field` |
|
|
267
|
+
| `min: field` | MIN(field) | `min_field` |
|
|
268
|
+
| `max: field` | MAX(field) | `max_field` |
|
|
269
|
+
| `count` | COUNT(*) | `count` |
|
|
270
|
+
|
|
271
|
+
Multiple fields per function: `sum: Freight, Total` → `sum_Freight` and `sum_Total`.
|
|
272
|
+
|
|
273
|
+
| Frontmatter | Meaning |
|
|
274
|
+
| --- | --- |
|
|
275
|
+
| `sort: sum_revenue desc, EmployeeID asc` | ORDER BY (multiple columns, mixed) |
|
|
276
|
+
| `having: count > 5` | HAVING COUNT(*) > 5 |
|
|
277
|
+
| `having: sum_Freight > 1000, count > 2` | HAVING … AND … (comma = AND) |
|
|
278
|
+
|
|
279
|
+
`having:` supports: `>`, `>=`, `<`, `<=`, `=`.
|
|
280
|
+
`sort:` references any result column — grouping keys or aggregate aliases.
|
|
281
|
+
`page-size:` and `page:` apply to the aggregated result set.
|
|
282
|
+
|
|
283
|
+
**Example — top 3 employees by revenue:**
|
|
284
|
+
|
|
285
|
+
```text
|
|
286
|
+
read("group: EmployeeID\nsum: revenue\nsort: sum_revenue desc\npage-size: 3\n\n#? OrderDetails")
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Error Handling
|
|
290
|
+
|
|
291
|
+
All tools return a `# Error` document on failure:
|
|
292
|
+
|
|
293
|
+
```text
|
|
294
|
+
# Error
|
|
295
|
+
status: 400
|
|
296
|
+
code: not_found
|
|
297
|
+
message: No records found in Orders
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Check the `code` field to decide how to proceed.
|
|
301
|
+
|
|
302
|
+
## Specification
|
|
303
|
+
|
|
304
|
+
The JMD format is documented at [jmd-spec](https://github.com/ostermeyer/jmd-spec).
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT License. See [LICENSE](LICENSE).
|