Skip to content

Commit ccfbe9e

Browse files
committed
feat(server): add CRUD-aware interceptor system for Rust server (#116)
Enrich the server interceptor pipeline with CRUD context so interceptors can distinguish entity insert from relation delete from raw query — enabling rate limiting, tenant filtering, and CRUD-aware audit logging at the HTTP layer. - CrudInfo struct with operation/type_name/type_kind/attribute_names/iid - CrudInterceptor convenience trait + CrudInterceptorAdapter bridge - RequestContext and QueryInput carry crud_info through the pipeline - All 8 CRUD handlers populate CrudInfo; non-CRUD uses Default - PipelineBuilder::with_skip_validation() for tests needing schema without validation - 100% MC/DC coverage on all changed files (regions/functions/lines) - crud_interceptor.rs branches at 87.5% (1 missed: generic monomorphization artifact) - 295 tests total (25 new)
1 parent 4f4bd8d commit ccfbe9e

23 files changed

Lines changed: 189 additions & 3060 deletions

CHANGELOG.md

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,103 @@
22

33
All notable changes to TypeBridge will be documented in this file.
44

5+
## [Unreleased]
6+
7+
### New Features
8+
9+
#### Put (Upsert) Clause — `type-bridge-core-lib`
10+
- **Added `Clause::Put(Vec<Statement>)` variant** for idempotent insert (upsert) operations
11+
- Parser: `parse_put_clause` with keyword lookahead in both `parse_patterns` and `parse_statements`
12+
- Compiler: Generates `put\n<statements>;` TypeQL output
13+
- Validation: Reuses Insert validation rules (`Clause::Insert | Clause::Put`)
14+
- Full roundtrip support (parse → AST → compile → parse)
15+
16+
### Refactoring
17+
18+
#### Server API Simplification — `type-bridge-server`
19+
- **Removed CRUD convenience endpoints** (`/entities/*`, `/relations/*`) — the `/query` endpoint handles all AST-based queries
20+
- **Removed `/query/raw` endpoint** — raw TypeQL bypasses AST interception, defeating the middleware purpose
21+
- **Removed `CrudInfo` and `CrudInterceptor`** — built exclusively for CRUD handlers, dead weight without them
22+
- **Removed `execute_raw()` and `RawQueryInput`** from pipeline
23+
- **Removed `RawQueryRequest`** from transport types
24+
- **Removed `crud_builder` benchmark** and `criterion` dev-dependency
25+
- **Server now has 4 endpoints**: `POST /query`, `POST /query/validate`, `GET /health`, `GET /schema`
26+
527
## [1.4.0] - 2026-02-20
628

29+
### Highlights
30+
31+
- 5 new Rust crates: `core-lib`, `orm`, `orm-derive`, `server`, and `python` (PyO3 bindings)
32+
- Up to **40x faster validation** and **2.5x faster query compilation** via the Rust backend
33+
- Async Rust ORM with derive macros, chainable queries, and batch operations
34+
- Query-intercepting proxy server with REST endpoints
35+
- MkDocs Material documentation site
36+
737
### New Features
838

9-
#### Rust Core Integration (PRs #95, #101#107)
39+
#### Rust Core `type-bridge-core-lib` (PRs #95, #101-#108)
1040
- **TypeQL schema parser** with inheritance resolution and PyO3 bindings
1141
- **TypeQL query parser** with bidirectional AST roundtrip
12-
- **Schema-aware query validation** with PyO3 bindings
42+
- **Schema-aware query validation** with statement and pattern validators
1343
- **Rust-backed value coercion** and `format_value`
1444
- **Custom validation rules** with portable JSON DSL
1545
- **Wired Rust core into Python** compiler and validation pipeline
1646

17-
#### Rust ORM — `type-bridge-orm` (PR #114)
18-
- **Async Rust ORM** with entity CRUD and mock-testable session layer
19-
- **Derive macros**`TypeBridgeEntity`, `TypeBridgeAttribute`, `TypeBridgeRelation`
20-
- **Relation support** with update/put operations
47+
#### Async Rust ORM — `type-bridge-orm` (PR #114)
48+
- **Async ORM** with entity CRUD and mock-testable session layer
49+
- **Derive macros**`#[derive(TypeBridgeEntity)]`, `#[derive(TypeBridgeRelation)]`, `#[derive(TypeBridgeAttribute)]`
2150
- **Chainable query builders** with expression filtering and aggregation
2251
- **Schema management** — registration, generation, diff, and sync
23-
- **Abstract types, inheritance, and code generator**
52+
- **Abstract types** with inheritance and code generation
2453
- **Batch operations**`insert_many`, `delete_many`, `update_many`
2554
- **`FieldRef<A>`** for type-safe query field references
26-
- **`include_schema!` proc-macro** for compile-time TQL codegen
55+
- **`include_schema!`** proc-macro for compile-time TQL codegen
2756
- **Schema introspection** from live TypeDB database
2857
- **Group-by queries** with `GroupByResult`
2958
- **Role player field access** for relation query filtering
30-
- **Expression helpers**`in_range`, `startswith`, `endswith`
31-
- **Connection pooling** with `Database::into_shared`
59+
- **Expression helpers**`in_range()`, `startswith()`, `endswith()`
60+
- **Connection pooling** with `Database::into_shared()`
3261
- **Serde support** on all ORM model types
33-
- **Structured tracing spans** on all public methods
62+
- **Structured tracing** spans on all public methods
3463

35-
#### Query Intercept Proxy Server `type-bridge-server` (PR #109)
64+
#### Query Intercept Proxy — `type-bridge-server` (PR #109)
3665
- **REST CRUD endpoints** with schema-aware query building
37-
- **`CrudQueryBuilder` PyO3 class** for TypeQL generation
38-
- **Extensible library/framework architecture**
39-
- **207 tests** with 100% MC/DC coverage and CI codecov integration
66+
- **`CrudQueryBuilder` PyO3 class** for TypeQL generation from Python
67+
- **Extensible library/framework** — pluggable `QueryExecutor`, `Interceptor`, `SchemaSource`
68+
- **207 tests** with 100% MC/DC coverage
4069

41-
### Improvements
70+
### Performance: Python vs Rust
71+
72+
#### Validation
73+
74+
| Operation | Python | Rust | Speedup |
75+
|-----------|--------|------|---------|
76+
| Single type name | 1.89 us | 354.75 ns | **5.3x** |
77+
| Long name (100+ chars) | 24.90 us | 620.52 ns | **40.1x** |
78+
| Batch 1,000 names | 5.32 ms | 266.80 us | **19.9x** |
79+
| Batch 5,000 names | 28.02 ms | 1.33 ms | **21.1x** |
80+
81+
#### Query Compilation (via serde bridge)
82+
83+
| Operation | Python | Rust | Speedup |
84+
|-----------|--------|------|---------|
85+
| Standalone update | 93.28 us | 37.82 us | **2.5x** |
86+
| Large batch (200 clauses) | 2.26 ms | 1.07 ms | **2.1x** |
87+
| Heavy insert (100x6) | 1.71 ms | 896.19 us | **1.9x** |
88+
89+
### Documentation & CI
4290

43-
#### Documentation & CI
4491
- **MkDocs + Material** documentation site with auto-generated API reference (PR #98)
45-
- **Rust crate CI** and multi-platform wheel builds (PR #95)
46-
- **Comprehensive benchmark suite** with TOML storage and diff support (PR #103)
47-
- Full documentation and metadata polish for Rust core
92+
- **Rust crate CI** with multi-platform wheel builds — Linux (x86_64, aarch64), macOS (x86_64, aarch64), Windows (x86_64) (PR #95)
93+
- **Comprehensive benchmark suite** with TOML storage, comparison reports, and markdown generation (PR #103)
94+
- **Codecov integration** for Rust coverage tracking (PR #109)
4895

4996
### Bug Fixes
5097

5198
- Resolve Rust 1.93.0 clippy lint errors
5299
- Pin Python 3.13 for Rust CI jobs and fix coverage script
100+
- Add version specifiers to inter-crate path dependencies for crates.io publishing
101+
- Make release workflow idempotent (skip already-published packages)
53102

54103
## [1.3.0] - 2026-02-09
55104

type-bridge-core/Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

type-bridge-core/crates/core/src/ast.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,11 @@ pub enum Clause {
235235
MatchLet(Vec<LetAssignment>),
236236
/// An insert clause containing statements that create new data.
237237
Insert(Vec<Statement>),
238+
/// A put clause containing statements for idempotent insert (upsert).
239+
///
240+
/// Semantics: insert if not exists, otherwise return the existing match.
241+
/// Uses the same statement syntax as `Insert`.
242+
Put(Vec<Statement>),
238243
/// A delete clause containing statements that remove existing data.
239244
Delete(Vec<Statement>),
240245
/// An update clause containing statements that modify existing data.

type-bridge-core/crates/core/src/compiler.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ impl QueryCompiler {
4343
.join(";\n");
4444
format!("insert\n{};", s_str)
4545
}
46+
Clause::Put(statements) => {
47+
let s_str = statements.iter()
48+
.map(|s| self.compile_statement(s))
49+
.collect::<Vec<_>>()
50+
.join(";\n");
51+
format!("put\n{};", s_str)
52+
}
4653
Clause::Delete(statements) => {
4754
let s_str = statements.iter()
4855
.map(|s| self.compile_statement(s))
@@ -660,6 +667,16 @@ mod tests {
660667
assert_eq!(c.compile_clause(&clause), "insert\n$p has name \"Alice\";");
661668
}
662669

670+
#[test]
671+
fn test_clause_put() {
672+
let c = compiler();
673+
let clause = Clause::Put(vec![
674+
Statement::Isa { variable: "$p".into(), type_name: "person".into() },
675+
Statement::Has { subject_var: "$p".into(), attr_name: "name".into(), value: lit(json!("Alice"), "string") },
676+
]);
677+
assert_eq!(c.compile_clause(&clause), "put\n$p isa person;\n$p has name \"Alice\";");
678+
}
679+
663680
#[test]
664681
fn test_clause_delete() {
665682
let c = compiler();
@@ -856,6 +873,18 @@ mod tests {
856873
assert!(result.contains("insert\n"));
857874
}
858875

876+
#[test]
877+
fn test_multi_clause_match_put() {
878+
let c = compiler();
879+
let clauses = vec![
880+
Clause::Match(vec![Pattern::Entity { variable: "$p".into(), type_name: "person".into(), constraints: vec![], is_strict: false }]),
881+
Clause::Put(vec![Statement::Has { subject_var: "$p".into(), attr_name: "age".into(), value: lit(json!(30), "long") }]),
882+
];
883+
let result = c.compile(&clauses);
884+
assert!(result.starts_with("match\n"));
885+
assert!(result.contains("put\n"));
886+
}
887+
859888
#[test]
860889
fn test_multi_clause_match_delete() {
861890
let c = compiler();
@@ -968,6 +997,16 @@ mod tests {
968997
roundtrip("insert\n$r isa employment, links (employee: $p, employer: $c);");
969998
}
970999

1000+
#[test]
1001+
fn test_roundtrip_put() {
1002+
roundtrip("put\n$p isa person;\n$p has name \"Alice\";");
1003+
}
1004+
1005+
#[test]
1006+
fn test_roundtrip_match_put() {
1007+
roundtrip("match $p isa person, has name \"Alice\";\nput\n$p has age 30;");
1008+
}
1009+
9711010
#[test]
9721011
fn test_roundtrip_match_delete() {
9731012
roundtrip("match $p isa person, has name \"Alice\";\ndelete $p;");

type-bridge-core/crates/core/src/query_parser.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,7 @@ fn parse_patterns(input: &mut &str) -> PResult<Vec<Pattern>> {
590590
ws_comments(input);
591591
if input.is_empty()
592592
|| input.starts_with("insert")
593+
|| input.starts_with("put")
593594
|| input.starts_with("delete")
594595
|| input.starts_with("update")
595596
|| input.starts_with("fetch")
@@ -897,6 +898,7 @@ fn parse_statements(input: &mut &str, ctx: StmtContext) -> PResult<Vec<Statement
897898
if input.is_empty()
898899
|| input.starts_with("match")
899900
|| input.starts_with("insert")
901+
|| input.starts_with("put")
900902
|| input.starts_with("delete")
901903
|| input.starts_with("update")
902904
|| input.starts_with("fetch")
@@ -1202,6 +1204,7 @@ fn parse_clause(input: &mut &str) -> PResult<Clause> {
12021204
alt((
12031205
parse_match_clause,
12041206
parse_insert_clause,
1207+
parse_put_clause,
12051208
parse_delete_clause,
12061209
parse_update_clause,
12071210
parse_fetch_clause,
@@ -1259,6 +1262,14 @@ fn parse_insert_clause(input: &mut &str) -> PResult<Clause> {
12591262
Ok(Clause::Insert(stmts))
12601263
}
12611264

1265+
/// Parse a put clause: `put\n<statements>;`.
1266+
fn parse_put_clause(input: &mut &str) -> PResult<Clause> {
1267+
literal("put").parse_next(input)?;
1268+
ws_comments(input);
1269+
let stmts = parse_statements(input, StmtContext::Insert)?;
1270+
Ok(Clause::Put(stmts))
1271+
}
1272+
12621273
/// Parse a delete clause: `delete\n<statements>;`.
12631274
fn parse_delete_clause(input: &mut &str) -> PResult<Clause> {
12641275
literal("delete").parse_next(input)?;
@@ -2129,6 +2140,28 @@ mod tests {
21292140
}
21302141
}
21312142

2143+
#[test]
2144+
fn test_parse_put_clause() {
2145+
let input = "put\n$p isa person;\n$p has name \"Alice\";";
2146+
let clauses = parse_typeql_query(input).unwrap();
2147+
assert_eq!(clauses.len(), 1);
2148+
match &clauses[0] {
2149+
Clause::Put(stmts) => {
2150+
assert_eq!(stmts.len(), 2);
2151+
}
2152+
_ => panic!("expected Put"),
2153+
}
2154+
}
2155+
2156+
#[test]
2157+
fn test_parse_match_put() {
2158+
let input = "match\n$p isa person;\nput\n$p has age 30;";
2159+
let clauses = parse_typeql_query(input).unwrap();
2160+
assert_eq!(clauses.len(), 2);
2161+
assert!(matches!(&clauses[0], Clause::Match(_)));
2162+
assert!(matches!(&clauses[1], Clause::Put(_)));
2163+
}
2164+
21322165
#[test]
21332166
fn test_parse_delete_clause() {
21342167
let input = "delete\n$p;";

type-bridge-core/crates/core/src/validation.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,7 @@ impl ValidationEngine {
654654
);
655655
}
656656
}
657-
Clause::Insert(stmts) => {
657+
Clause::Insert(stmts) | Clause::Put(stmts) => {
658658
self.validate_insert_stmts(stmts, schema, env, path, errors);
659659
}
660660
Clause::Delete(stmts) | Clause::Update(stmts) => {
@@ -2161,6 +2161,44 @@ mod schema_validation_tests {
21612161
assert!(!result.is_valid);
21622162
assert!(result.errors.iter().any(|e| e.code == "UNKNOWN_ATTRIBUTE_OWNERSHIP"));
21632163
}
2164+
2165+
// -- Put clause validation (same semantics as Insert) --------------------
2166+
2167+
#[test]
2168+
fn test_put_valid() {
2169+
let engine = ValidationEngine::new();
2170+
let schema = build_test_schema();
2171+
let clauses = vec![Clause::Put(vec![
2172+
Statement::Isa { variable: "$p".into(), type_name: "person".into() },
2173+
Statement::Has { subject_var: "$p".into(), attr_name: "name".into(), value: Value::Literal(LiteralValue { value: json!("Alice"), value_type: "string".into() }) },
2174+
])];
2175+
let result = engine.validate_query(&clauses, &schema);
2176+
assert!(result.is_valid);
2177+
}
2178+
2179+
#[test]
2180+
fn test_put_unknown_type() {
2181+
let engine = ValidationEngine::new();
2182+
let schema = build_test_schema();
2183+
let clauses = vec![Clause::Put(vec![
2184+
Statement::Isa { variable: "$x".into(), type_name: "spaceship".into() },
2185+
])];
2186+
let result = engine.validate_query(&clauses, &schema);
2187+
assert!(!result.is_valid);
2188+
assert!(result.errors.iter().any(|e| e.code == "UNKNOWN_TYPE"));
2189+
}
2190+
2191+
#[test]
2192+
fn test_put_abstract_type() {
2193+
let engine = ValidationEngine::new();
2194+
let schema = build_test_schema();
2195+
let clauses = vec![Clause::Put(vec![
2196+
Statement::Isa { variable: "$a".into(), type_name: "animal".into() },
2197+
])];
2198+
let result = engine.validate_query(&clauses, &schema);
2199+
assert!(!result.is_valid);
2200+
assert!(result.errors.iter().any(|e| e.code == "ABSTRACT_TYPE_INSTANTIATION"));
2201+
}
21642202
}
21652203

21662204
#[cfg(test)]
@@ -2398,4 +2436,5 @@ mod rule_tests {
23982436
assert!(engine.validate_type_name("person", "entity").is_valid);
23992437
assert!(!engine.validate_type_name("define", "entity").is_valid);
24002438
}
2439+
24012440
}

type-bridge-core/crates/server/Cargo.toml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@ tower-http = { version = "0.6", features = ["cors", "trace"], optional = true }
4545
tempfile = "3"
4646
http-body-util = "0.1"
4747
tower = { version = "0.5", features = ["util"] }
48-
criterion = { version = "0.5", features = ["html_reports"] }
49-
50-
[[bench]]
51-
name = "crud_builder"
52-
harness = false
5348

5449
[lints]
5550
workspace = true

0 commit comments

Comments
 (0)