# qql-core
Transport-free QQL frontend: lexer, parser, typed AST, validation,
`inject_filter`, and explain. **No I/O, no Qdrant JSON.**
Canonical grammar: [`language/v1/grammar.pest`](https://github.com/srimon12/qql-rs/blob/main/language/v1/grammar.pest)
→ `qql-grammar-gen` → checked-in `grammar/` (do not edit by hand).
## Proposition
Own the **language surface** for every SDK. Hosts and agents parse here, inject
policy here, then hand `Stmt` to `qql-plan` / runtime.
## Statement surface
| Query | `QUERY` (nearest, hybrid, formula, recommend, `RERANK`, **`CROSS RERANK`**, …), CTEs |
| Retrieval | `SCROLL`, `COUNT`, **`FACET`** |
| DML | `UPSERT`, `DELETE`, payload/vector updates (`DELETE PAYLOAD`, …) |
| DDL | `CREATE/ALTER/DROP COLLECTION`, indexes, **`CREATE/DROP/SHOW SHARD KEY`** |
| Meta | `SHOW COLLECTIONS` / `SHOW COLLECTION` |
| Quotas | **`SHOW QUOTAS`** / **`SET QUOTA (…)`** (Qdrant ≥ 1.19; REST at execute time) |
### Clause order (QUERY)
```
QUERY <expr> FROM <coll>
[USING HYBRID … | USING <vec> [AS DENSE|SPARSE|MULTI]]
[PREFETCH (…)] [WHERE …] [SHARD '…'] [PARAMS (…)]
[SCORE THRESHOLD n] [GROUP BY …] [WITH PAYLOAD|VECTOR …]
[LIMIT n] [OFFSET n]
```
### `SHARD KEY` vs `SHARD`
| `CREATE SHARD KEY 'acme' ON COLLECTION c` | DDL — define custom partition |
| `… SHARD 'acme'` | DML — route this request |
| `… SHARD 101` | DML — route to a numeric partition (never coerced to `"101"`) |
Routing field after parse: `stmt.set_shard_key(Some("acme".into()))`
(same AST field; **no** `inject_shard_key`).
### PARAMS (selected)
Body search params: `hnsw_ef`, `exact`, `acorn`, `max_selectivity`, `quantization`,
`idf` (`'global'` or `WHERE <filter>` for sparse IDF, Qdrant ≥ 1.19), …
**Request-level** (REST query string / gRPC fields): `timeout`, `consistency`.
### Filters (selected, Qdrant ≥ 1.19)
```sql
WHERE title MATCH PREFIX 'Comp' -- keyword prefix (needs prefix=true index)
WHERE SLICE (4, 1) -- hash(id) % total == index; total≥1, index<total
```
### Typed config enums
| `MemoryPlacement` | `cold` / `cached` / `pinned` | `WITH HNSW|VECTOR|SPARSE|QUANTIZATION`, index options; `payload_memory` rejects `pinned` |
| `VectorDatatype` | `float32` / `float16` / `uint8` / `turbo4` | dense `WITH VECTOR (datatype = …)`; sparse rejects `turbo4` |
```sql
CREATE COLLECTION docs (
dense VECTOR(384, COSINE)
WITH VECTOR (memory = 'cached', datatype = 'turbo4')
) WITH HNSW (memory = 'cold')
WITH PARAMS (payload_memory = 'cold');
CREATE INDEX ON COLLECTION docs FOR title TYPE keyword
WITH (prefix = true, memory = 'cached');
SHOW QUOTAS;
SET QUOTA (enabled = true, max_resident_memory_percent = 80) WAIT true;
```
## API
```rust
use qql_core::ast::{inject_filter, ComparisonOp, Stmt, Value};
use qql_core::parser::Parser;
let mut stmt = Parser::parse(
"QUERY TEXT 'hello' FROM docs USING dense LIMIT 5"
)?;
// Isolation (recurses CTEs / prefetches). Fail-closed on DDL / SHOW /
// UPDATE VECTOR, and on UPSERT unless Eq on a non-id payload field.
// ComparisonOp::parse_inject_op is ASCII-case-insensitive (`EQ`, ` Gt `).
inject_filter(
&mut stmt,
"tenant_id",
ComparisonOp::Eq,
Value::Str("org_99".into()),
)?;
// Optional routing (custom sharding)
stmt.set_shard_key(Some("org_99".into()));
// Prefer authoring: ... SHARD 'org_99' LIMIT 5
```
| `serde` | AST serialize |
| `json` | `Value` from/to JSON |
| `std` | `std::error::Error` |
## Docs
- [Syntax](https://github.com/srimon12/qql-rs/blob/main/docs/syntax.md) · [inject_filter](https://github.com/srimon12/qql-rs/blob/main/docs/inject_filter.md) · [Multitenancy](https://github.com/srimon12/qql-rs/blob/main/skills/qql-skill/references/qql-multitenancy.md)
## Test
```bash
cargo test -p qql-core
```