rust-store-core
One data engine for MongoDB, MySQL, PostgreSQL and SQLite. A MongoDB-style query language (GQL), permission checks, computed columns, command planning and SQL dialect translation — as a plain Rust library that opens no connection, starts no clock and generates no randomness.
rust-store-core is the engine behind rust-store-node (napi-rs) and rust-store-py (PyO3), and through them behind nodejs-store and py-store.
Install
What it does
| Step | Function | Input → output |
|---|---|---|
| Parse + plan | command::plan_query |
GQL + params + registry + context → QueryPlan (MongoDB command JSON) |
| Translate | dialect::translate |
command JSON + Backend → parameterized SQL statements |
| Rehydrate | dialect::restore_rows_json |
flat JOIN rows → nested documents |
The host is the only IO boundary: it takes the planned command, runs it against its own driver, and hands the rows back for rehydration.
Three guarantees
- No IO, no clock, no randomness.
nowandnew_idsare always passed in by the caller. This is what makes the engine deterministic and reproducible across languages. - Explicit over silent. Anything that cannot be translated safely returns an error or an
unsupportedmarker plus warnings. The engine never emits SQL that is quietly missing a clause. - JSON in, JSON out. The Node.js and Python bindings only convert and forward, so the two hosts cannot drift apart semantically.
Naming & translation
Canonicalization has a single implementation in core::naming, re-exported by core-node / core-py; hosts must not re-implement it.
Naming: freeform definitions, system-directed translation
Definitions (collection, fields, referenced relation fields, computed-column keys, fnRef values, index names) may use any style; the engine translates them to the target style. Contract keys (fnRef, localField, foreignField, asyncFn, type, ...) and the schema name are never translated.
| Target | Style | Example (orderTotal) |
|---|---|---|
| MySQL / PostgreSQL / SQLite (physical) | snake_case | order_total |
| MongoDB (physical) | camelCase | orderTotal |
| Node.js / Java / C# / Rust (code; computed columns follow) | camelCase | orderTotal |
| Go (code; computed columns follow) | PascalCase (must be exported) | OrderTotal |
| Python (code; computed columns follow) | snake_case | order_total |
Canonicalization (single implementation core::naming, re-exported by the bindings; hosts must not re-implement it): split on _, -, ., space and at lower/digit-to-upper boundaries; a trailing uppercase in a run followed by a lowercase starts the next token (HTTPServer -> [http, server], userID -> [user, id]); digits stay inside a token (order2Items -> [order2, items]). Reassembly: snake = t1_t2, camel = t1T2, pascal = T1T2.
Two logical names in one schema that canonicalize equal (orderTotal vs order_total), or a name that canonicalizes onto a reserved contract key (e.g. fnref), is an error ERR_NAME_CONFLICT: and the service does not start (never silently overwritten).
Computed columns: fnRef binding by composite name + canonical match
Computed columns live at the schema top level, computes: { <key>: { type, fn | asyncFn | agg, fnRef?, depends?, read? } } (fn / asyncFn / agg are mutually exclusive).
- The logical
fnRefdefaults to<schema.name>.<computed-column key>(generated, never hand-written); sincenameis globally unique, thefnRefis globally unique too. - Host implementations bind by canonicalization: both the implementation's name in the host language style and the schema's logical
fnRefare canonicalized to token sequences and compared. So Node'sorderAmountLabeland Python'sorder_amount_labelbind to the same logical computed column. - Reusing one implementation across schemas: write an explicit shared name (e.g.
"fnRef": "common.moneyLabel"); naming goes from required to optional. - Every declared
fnRefmust have an implementation, otherwise the service fails to start withERR_FN_MISSING.
Quickstart
use ;
use plan_query;
use ;
use Registry;
let mut registry = new;
registry.register?;
let gql = "Post($condition:@c0,$sort:@s1,$limit:@l0){title, status, views}";
let mut params = new;
params.insert;
params.insert;
params.insert;
let plan = plan_query?;
for cmd in &plan.commands
A runnable version lives in examples/quickstart.rs — cargo run --example quickstart.
GQL in one look
ModelName($condition:@c0,$sort:@s1,$skip:@sk,$limit:@l1) {
field1, field2, obj.subField,
RelationName($condition:@c2,$sort:@s3,$limit:@l2) { field3, NestedRelation { field4 } }
}
Values are referenced from the params object by @key. Relations are declared in the schema and compiled by the engine into $lookup / $addFields — you never hand-write $lookup. Root-level $group / $having are supported, with the same operator whitelist ($count / $sum / $avg / $min / $max) on both the Mongo and the SQL side.
Documentation
- Full documentation site — https://coenddt.github.io/rust-store/ — scenario walkthroughs with runnable code and the engine's exact limits, one indexable page per scenario.
- API reference — https://docs.rs/rust-store-core
- Repository — https://github.com/coenddt/rust-store
License
MIT