create-feltdb 0.11.3 → 0.11.7
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.
- package/dist/package-versions.js +1 -1
- package/dist/server-source/crates/feltdb/src/lib.rs +10 -1
- package/dist/server-source/crates/feltdb/src/semantic_decision.rs +124 -12
- package/dist/server-source/crates/feltdb/src/semantic_query.rs +599 -0
- package/dist/server-source/crates/feltdb/src/state_contract.rs +222 -0
- package/dist/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +1 -0
- package/dist/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +1 -0
- package/dist/server-source/crates/feltdb/tests/semantic_query.rs +767 -0
- package/dist/server-source/crates/feltdb/tests/semantic_query_contract.rs +363 -0
- package/dist/server-source/crates/feltdb-server/src/app_state.rs +5 -0
- package/dist/server-source/crates/feltdb-server/src/main.rs +151 -3
- package/package.json +1 -1
|
@@ -740,6 +740,12 @@ pub struct CanonicalQuery {
|
|
|
740
740
|
pub aggregates: Vec<AggregateDefinition>,
|
|
741
741
|
#[serde(default)]
|
|
742
742
|
pub references: Vec<QueryReference>,
|
|
743
|
+
/// A semantic clause is a recognized query concept that this surface does
|
|
744
|
+
/// not compose. It is carried through deserialization so `execute_query`
|
|
745
|
+
/// can refuse it with a structured failure instead of an unknown key
|
|
746
|
+
/// silently disappearing; see `docs/reference/semantic-query.md`.
|
|
747
|
+
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
748
|
+
pub semantic: Option<Value>,
|
|
743
749
|
}
|
|
744
750
|
/// Actual execution timing measurements for a query.
|
|
745
751
|
/// All fields are MEASURED using Instant::now(), not estimated or derived.
|
|
@@ -1198,6 +1204,20 @@ pub fn execute_query(
|
|
|
1198
1204
|
) -> Result<QueryResult, StateFailure> {
|
|
1199
1205
|
let query_start = Instant::now();
|
|
1200
1206
|
|
|
1207
|
+
// Fail closed on semantic intent before any deterministic work. Every query
|
|
1208
|
+
// surface must either implement a recognized semantic clause per the
|
|
1209
|
+
// semantic-query contract or refuse it; this surface refuses, and it never
|
|
1210
|
+
// answers the deterministic part as though the clause had been absent.
|
|
1211
|
+
if let Some(clause) = &query.semantic {
|
|
1212
|
+
return Err(if clause.is_object() {
|
|
1213
|
+
StateFailure::new(
|
|
1214
|
+
"SEMANTIC_UNSUPPORTED",
|
|
1215
|
+
"canonical queries recognize the semantic clause but do not compose semantic decisions; use the bounded authority query",
|
|
1216
|
+
)
|
|
1217
|
+
} else {
|
|
1218
|
+
StateFailure::new("INVALID_QUERY", "semantic clause must be an object")
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1201
1221
|
if query.limit == Some(0) || query.limit.is_some_and(|limit| limit > 1000) {
|
|
1202
1222
|
return Err(StateFailure::new(
|
|
1203
1223
|
"INVALID_QUERY",
|
|
@@ -3205,6 +3225,7 @@ mod tests {
|
|
|
3205
3225
|
field: None,
|
|
3206
3226
|
}],
|
|
3207
3227
|
references: vec![],
|
|
3228
|
+
semantic: None,
|
|
3208
3229
|
},
|
|
3209
3230
|
None,
|
|
3210
3231
|
)
|
|
@@ -3236,6 +3257,7 @@ mod tests {
|
|
|
3236
3257
|
group_by: vec![],
|
|
3237
3258
|
aggregates: vec![],
|
|
3238
3259
|
references: vec![],
|
|
3260
|
+
semantic: None,
|
|
3239
3261
|
},
|
|
3240
3262
|
None,
|
|
3241
3263
|
)
|
|
@@ -3498,6 +3520,7 @@ mod tests {
|
|
|
3498
3520
|
group_by: vec![],
|
|
3499
3521
|
aggregates: vec![],
|
|
3500
3522
|
references: vec![],
|
|
3523
|
+
semantic: None,
|
|
3501
3524
|
},
|
|
3502
3525
|
None,
|
|
3503
3526
|
)
|
|
@@ -3592,6 +3615,7 @@ mod tests {
|
|
|
3592
3615
|
group_by: vec![],
|
|
3593
3616
|
aggregates: vec![],
|
|
3594
3617
|
references: vec![],
|
|
3618
|
+
semantic: None,
|
|
3595
3619
|
},
|
|
3596
3620
|
None,
|
|
3597
3621
|
)
|
|
@@ -3654,6 +3678,7 @@ mod tests {
|
|
|
3654
3678
|
group_by: vec![],
|
|
3655
3679
|
aggregates: vec![],
|
|
3656
3680
|
references: vec![],
|
|
3681
|
+
semantic: None,
|
|
3657
3682
|
},
|
|
3658
3683
|
None,
|
|
3659
3684
|
)
|
|
@@ -3715,6 +3740,7 @@ mod tests {
|
|
|
3715
3740
|
group_by: vec![],
|
|
3716
3741
|
aggregates: vec![],
|
|
3717
3742
|
references: vec![],
|
|
3743
|
+
semantic: None,
|
|
3718
3744
|
},
|
|
3719
3745
|
None,
|
|
3720
3746
|
)
|
|
@@ -3774,6 +3800,7 @@ mod tests {
|
|
|
3774
3800
|
group_by: vec![],
|
|
3775
3801
|
aggregates: vec![],
|
|
3776
3802
|
references: vec![],
|
|
3803
|
+
semantic: None,
|
|
3777
3804
|
},
|
|
3778
3805
|
None,
|
|
3779
3806
|
)
|
|
@@ -3834,6 +3861,7 @@ mod tests {
|
|
|
3834
3861
|
group_by: vec![],
|
|
3835
3862
|
aggregates: vec![],
|
|
3836
3863
|
references: vec![],
|
|
3864
|
+
semantic: None,
|
|
3837
3865
|
};
|
|
3838
3866
|
let mut ids = Vec::new();
|
|
3839
3867
|
let first_cursor = loop {
|
|
@@ -3888,6 +3916,186 @@ mod tests {
|
|
|
3888
3916
|
);
|
|
3889
3917
|
}
|
|
3890
3918
|
|
|
3919
|
+
/// Every query surface must either implement a recognized semantic clause
|
|
3920
|
+
/// per `docs/reference/semantic-query.md` or refuse it. This surface
|
|
3921
|
+
/// refuses: a semantic clause is `SEMANTIC_UNSUPPORTED` before any
|
|
3922
|
+
/// deterministic work, a malformed one is `INVALID_QUERY`, nothing is
|
|
3923
|
+
/// read, nothing is judged, and the same query without the clause still
|
|
3924
|
+
/// answers.
|
|
3925
|
+
#[test]
|
|
3926
|
+
fn semantic_clause_is_recognized_and_refused_never_ignored() {
|
|
3927
|
+
let db = db("semantic-refusal");
|
|
3928
|
+
let schema = schema();
|
|
3929
|
+
execute_transaction(
|
|
3930
|
+
&db,
|
|
3931
|
+
&schema,
|
|
3932
|
+
&TransactionRequest {
|
|
3933
|
+
transaction_id: Some("seed".into()),
|
|
3934
|
+
tenant_id: "tenant".into(),
|
|
3935
|
+
application_id: "app".into(),
|
|
3936
|
+
revision_id: "rev".into(),
|
|
3937
|
+
schema_version: 1,
|
|
3938
|
+
state_namespace: None,
|
|
3939
|
+
causal_parent: None,
|
|
3940
|
+
authorization: auth(),
|
|
3941
|
+
preconditions: vec![],
|
|
3942
|
+
operations: vec![TransactionOperation {
|
|
3943
|
+
kind: TransactionOperationKind::Insert,
|
|
3944
|
+
collection: "incidents".into(),
|
|
3945
|
+
id: "i1".into(),
|
|
3946
|
+
value: serde_json::json!({ "title": "First", "status": "open" }),
|
|
3947
|
+
if_version: None,
|
|
3948
|
+
}],
|
|
3949
|
+
},
|
|
3950
|
+
None,
|
|
3951
|
+
)
|
|
3952
|
+
.unwrap();
|
|
3953
|
+
let context = begin_query_read(&db, &schema, "app", auth()).unwrap();
|
|
3954
|
+
let plain = CanonicalQuery {
|
|
3955
|
+
collection: "incidents".into(),
|
|
3956
|
+
filter: Some(QueryFilter::Eq {
|
|
3957
|
+
field: "status".into(),
|
|
3958
|
+
value: Value::String("open".into()),
|
|
3959
|
+
}),
|
|
3960
|
+
order_by: vec![],
|
|
3961
|
+
limit: Some(10),
|
|
3962
|
+
offset: 0,
|
|
3963
|
+
cursor: None,
|
|
3964
|
+
projection: vec![],
|
|
3965
|
+
group_by: vec![],
|
|
3966
|
+
aggregates: vec![],
|
|
3967
|
+
references: vec![],
|
|
3968
|
+
semantic: None,
|
|
3969
|
+
};
|
|
3970
|
+
let state_before = db.state_rows().unwrap().len();
|
|
3971
|
+
|
|
3972
|
+
// A semantic clause in the bounded query's own grammar, and one that
|
|
3973
|
+
// only expresses semantic intent, are both recognized and refused.
|
|
3974
|
+
for clause in [
|
|
3975
|
+
serde_json::json!({
|
|
3976
|
+
"decision": { "kind": "binary", "predicate": "needs a human" },
|
|
3977
|
+
"filter": { "is": true },
|
|
3978
|
+
}),
|
|
3979
|
+
serde_json::json!({ "decision": "needs_human" }),
|
|
3980
|
+
serde_json::json!({}),
|
|
3981
|
+
] {
|
|
3982
|
+
let failure = execute_query(
|
|
3983
|
+
&db,
|
|
3984
|
+
&schema,
|
|
3985
|
+
&context,
|
|
3986
|
+
&CanonicalQuery {
|
|
3987
|
+
semantic: Some(clause),
|
|
3988
|
+
..plain.clone()
|
|
3989
|
+
},
|
|
3990
|
+
None,
|
|
3991
|
+
)
|
|
3992
|
+
.expect_err("semantic intent is never silently dropped");
|
|
3993
|
+
assert_eq!(failure.code, "SEMANTIC_UNSUPPORTED");
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
// A semantic value that is not a clause is a validation error, not a
|
|
3997
|
+
// recognized-but-unsupported operation.
|
|
3998
|
+
for malformed in [
|
|
3999
|
+
serde_json::json!("yes"),
|
|
4000
|
+
serde_json::json!(true),
|
|
4001
|
+
serde_json::json!(1),
|
|
4002
|
+
serde_json::json!(["needs_human"]),
|
|
4003
|
+
] {
|
|
4004
|
+
let failure = execute_query(
|
|
4005
|
+
&db,
|
|
4006
|
+
&schema,
|
|
4007
|
+
&context,
|
|
4008
|
+
&CanonicalQuery {
|
|
4009
|
+
semantic: Some(malformed),
|
|
4010
|
+
..plain.clone()
|
|
4011
|
+
},
|
|
4012
|
+
None,
|
|
4013
|
+
)
|
|
4014
|
+
.expect_err("a malformed semantic value is invalid");
|
|
4015
|
+
assert_eq!(failure.code, "INVALID_QUERY");
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
// The clause is refused ahead of every other validation, so a query
|
|
4019
|
+
// that is also otherwise invalid still fails on semantic intent.
|
|
4020
|
+
let failure = execute_query(
|
|
4021
|
+
&db,
|
|
4022
|
+
&schema,
|
|
4023
|
+
&context,
|
|
4024
|
+
&CanonicalQuery {
|
|
4025
|
+
semantic: Some(serde_json::json!({ "decision": "needs_human" })),
|
|
4026
|
+
limit: Some(0),
|
|
4027
|
+
..plain.clone()
|
|
4028
|
+
},
|
|
4029
|
+
None,
|
|
4030
|
+
)
|
|
4031
|
+
.unwrap_err();
|
|
4032
|
+
assert_eq!(failure.code, "SEMANTIC_UNSUPPORTED");
|
|
4033
|
+
|
|
4034
|
+
// Refusal reads nothing, judges nothing, and writes nothing: no
|
|
4035
|
+
// judgment collection appears and the state is what it was.
|
|
4036
|
+
assert_eq!(db.state_rows().unwrap().len(), state_before);
|
|
4037
|
+
let judgments: Vec<Value> = db
|
|
4038
|
+
.capability(crate::semantic_decision::DEFAULT_DECISION_COLLECTION)
|
|
4039
|
+
.find(|_: &Value| true)
|
|
4040
|
+
.unwrap();
|
|
4041
|
+
assert!(judgments.is_empty());
|
|
4042
|
+
|
|
4043
|
+
// The same query without the clause answers as before.
|
|
4044
|
+
let page = execute_query(&db, &schema, &context, &plain, None).unwrap();
|
|
4045
|
+
assert_eq!(page.records.len(), 1);
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
/// Over the wire, `semantic` is carried into the query rather than lost
|
|
4049
|
+
/// as an unknown key, `null` means absent, and an unrelated unknown key
|
|
4050
|
+
/// keeps its existing behavior of being ignored: only semantic intent is
|
|
4051
|
+
/// recognized, because only semantic intent changes what a query means.
|
|
4052
|
+
#[test]
|
|
4053
|
+
fn semantic_clause_survives_deserialization_and_changes_query_identity() {
|
|
4054
|
+
let plain: CanonicalQuery = serde_json::from_value(serde_json::json!({
|
|
4055
|
+
"collection": "incidents",
|
|
4056
|
+
"limit": 10,
|
|
4057
|
+
}))
|
|
4058
|
+
.unwrap();
|
|
4059
|
+
assert!(plain.semantic.is_none());
|
|
4060
|
+
let with_clause: CanonicalQuery = serde_json::from_value(serde_json::json!({
|
|
4061
|
+
"collection": "incidents",
|
|
4062
|
+
"limit": 10,
|
|
4063
|
+
"semantic": { "decision": "needs_human" },
|
|
4064
|
+
}))
|
|
4065
|
+
.unwrap();
|
|
4066
|
+
assert_eq!(
|
|
4067
|
+
with_clause.semantic,
|
|
4068
|
+
Some(serde_json::json!({ "decision": "needs_human" }))
|
|
4069
|
+
);
|
|
4070
|
+
assert_ne!(
|
|
4071
|
+
query_hash(&plain).unwrap(),
|
|
4072
|
+
query_hash(&with_clause).unwrap(),
|
|
4073
|
+
"a semantic clause is part of what the query is"
|
|
4074
|
+
);
|
|
4075
|
+
let malformed: CanonicalQuery = serde_json::from_value(serde_json::json!({
|
|
4076
|
+
"collection": "incidents",
|
|
4077
|
+
"semantic": "yes",
|
|
4078
|
+
}))
|
|
4079
|
+
.unwrap();
|
|
4080
|
+
assert_eq!(malformed.semantic, Some(Value::String("yes".into())));
|
|
4081
|
+
let null: CanonicalQuery = serde_json::from_value(serde_json::json!({
|
|
4082
|
+
"collection": "incidents",
|
|
4083
|
+
"semantic": null,
|
|
4084
|
+
}))
|
|
4085
|
+
.unwrap();
|
|
4086
|
+
assert!(null.semantic.is_none());
|
|
4087
|
+
let unrelated: CanonicalQuery = serde_json::from_value(serde_json::json!({
|
|
4088
|
+
"collection": "incidents",
|
|
4089
|
+
"limit": 10,
|
|
4090
|
+
"refill": true,
|
|
4091
|
+
}))
|
|
4092
|
+
.unwrap();
|
|
4093
|
+
assert_eq!(query_hash(&unrelated).unwrap(), query_hash(&plain).unwrap());
|
|
4094
|
+
// Serialization of a query without a clause is unchanged, so every
|
|
4095
|
+
// existing query hash and cursor binding is unchanged.
|
|
4096
|
+
assert!(!serde_json::to_string(&plain).unwrap().contains("semantic"));
|
|
4097
|
+
}
|
|
4098
|
+
|
|
3891
4099
|
#[test]
|
|
3892
4100
|
fn scoped_index_work_is_independent_of_unrelated_growth() {
|
|
3893
4101
|
fn examined(unrelated: usize) -> usize {
|
|
@@ -3941,6 +4149,7 @@ mod tests {
|
|
|
3941
4149
|
group_by: vec![],
|
|
3942
4150
|
aggregates: vec![],
|
|
3943
4151
|
references: vec![],
|
|
4152
|
+
semantic: None,
|
|
3944
4153
|
},
|
|
3945
4154
|
None,
|
|
3946
4155
|
)
|
|
@@ -4002,6 +4211,7 @@ mod tests {
|
|
|
4002
4211
|
group_by: vec![],
|
|
4003
4212
|
aggregates: vec![],
|
|
4004
4213
|
references: vec![],
|
|
4214
|
+
semantic: None,
|
|
4005
4215
|
},
|
|
4006
4216
|
None,
|
|
4007
4217
|
)
|
|
@@ -4143,6 +4353,7 @@ mod tests {
|
|
|
4143
4353
|
group_by: vec![],
|
|
4144
4354
|
aggregates: vec![],
|
|
4145
4355
|
references: vec![],
|
|
4356
|
+
semantic: None,
|
|
4146
4357
|
},
|
|
4147
4358
|
None,
|
|
4148
4359
|
)
|
|
@@ -4208,6 +4419,7 @@ mod tests {
|
|
|
4208
4419
|
group_by: vec![],
|
|
4209
4420
|
aggregates: vec![],
|
|
4210
4421
|
references: vec![],
|
|
4422
|
+
semantic: None,
|
|
4211
4423
|
};
|
|
4212
4424
|
|
|
4213
4425
|
let result = execute_query(&db, &schema, &context, &query, None);
|
|
@@ -4246,6 +4458,7 @@ mod tests {
|
|
|
4246
4458
|
group_by: vec![],
|
|
4247
4459
|
aggregates: vec![],
|
|
4248
4460
|
references: vec![],
|
|
4461
|
+
semantic: None,
|
|
4249
4462
|
};
|
|
4250
4463
|
|
|
4251
4464
|
let result = execute_query(&db, &schema, &context, &query, None);
|
|
@@ -4357,6 +4570,7 @@ mod tests {
|
|
|
4357
4570
|
group_by: vec![],
|
|
4358
4571
|
aggregates: vec![],
|
|
4359
4572
|
references: vec![],
|
|
4573
|
+
semantic: None,
|
|
4360
4574
|
};
|
|
4361
4575
|
|
|
4362
4576
|
let result = execute_query(&db, &schema, &context, &query, None);
|
|
@@ -4456,6 +4670,7 @@ mod tests {
|
|
|
4456
4670
|
group_by: vec![],
|
|
4457
4671
|
aggregates: vec![],
|
|
4458
4672
|
references: vec![],
|
|
4673
|
+
semantic: None,
|
|
4459
4674
|
};
|
|
4460
4675
|
|
|
4461
4676
|
let result = execute_query(&db, &schema, &context, &query, None);
|
|
@@ -4587,6 +4802,7 @@ mod tests {
|
|
|
4587
4802
|
group_by: vec![],
|
|
4588
4803
|
aggregates: vec![],
|
|
4589
4804
|
references: vec![],
|
|
4805
|
+
semantic: None,
|
|
4590
4806
|
};
|
|
4591
4807
|
|
|
4592
4808
|
let verify_result =
|
|
@@ -4705,6 +4921,7 @@ mod tests {
|
|
|
4705
4921
|
group_by: vec![],
|
|
4706
4922
|
aggregates: vec![],
|
|
4707
4923
|
references: vec![],
|
|
4924
|
+
semantic: None,
|
|
4708
4925
|
};
|
|
4709
4926
|
|
|
4710
4927
|
let result = execute_query(&db, &schema, &read_context, &query, None);
|
|
@@ -4743,6 +4960,7 @@ mod tests {
|
|
|
4743
4960
|
group_by: vec![],
|
|
4744
4961
|
aggregates: vec![],
|
|
4745
4962
|
references: vec![],
|
|
4963
|
+
semantic: None,
|
|
4746
4964
|
};
|
|
4747
4965
|
|
|
4748
4966
|
let result = execute_query(&db, &schema, &read_context, &query, None);
|
|
@@ -5009,6 +5227,7 @@ mod tests {
|
|
|
5009
5227
|
group_by: vec![],
|
|
5010
5228
|
aggregates: vec![],
|
|
5011
5229
|
references: vec![],
|
|
5230
|
+
semantic: None,
|
|
5012
5231
|
};
|
|
5013
5232
|
|
|
5014
5233
|
let alice_result = execute_query(
|
|
@@ -5104,6 +5323,7 @@ mod tests {
|
|
|
5104
5323
|
group_by: vec![],
|
|
5105
5324
|
aggregates: vec![],
|
|
5106
5325
|
references: vec![],
|
|
5326
|
+
semantic: None,
|
|
5107
5327
|
};
|
|
5108
5328
|
|
|
5109
5329
|
let result = execute_query(
|
|
@@ -5191,6 +5411,7 @@ mod tests {
|
|
|
5191
5411
|
group_by: vec![],
|
|
5192
5412
|
aggregates: vec![],
|
|
5193
5413
|
references: vec![],
|
|
5414
|
+
semantic: None,
|
|
5194
5415
|
};
|
|
5195
5416
|
|
|
5196
5417
|
let result = execute_query(
|
|
@@ -5280,6 +5501,7 @@ mod tests {
|
|
|
5280
5501
|
group_by: vec![],
|
|
5281
5502
|
aggregates: vec![],
|
|
5282
5503
|
references: vec![],
|
|
5504
|
+
semantic: None,
|
|
5283
5505
|
};
|
|
5284
5506
|
|
|
5285
5507
|
let result = execute_query(
|
|
@@ -336,6 +336,7 @@ fn pr7_phase3_self_authorization_proof() -> Result<(), Box<dyn std::error::Error
|
|
|
336
336
|
group_by: vec![],
|
|
337
337
|
aggregates: vec![],
|
|
338
338
|
references: vec![],
|
|
339
|
+
semantic: None,
|
|
339
340
|
};
|
|
340
341
|
|
|
341
342
|
// We'll need to manually filter since the policy evaluation isn't automatic in tests
|