qql-core 0.4.1

Parser, typed AST, validation, and transformations for the Qdrant Query Language
Documentation
# 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

| Kind | Examples |
|------|----------|
| 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`

| Form | Role |
|------|------|
| `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

| Type | Values | Use |
|------|--------|-----|
| `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
```

| Feature | Role |
|---------|------|
| `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
```