diff --git a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs index 2f2c97b09c1..bd37b545712 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/fee_pool_outwards_distribution/fetch_reward_shares_list_for_masternode/v0/mod.rs @@ -47,7 +47,7 @@ impl Platform { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: BTreeMap::from([( "$ownerId".to_string(), diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index e3a5cd227d0..066658c6013 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -1327,6 +1327,7 @@ mod tests { "select * from profile", &dashpay_v2_fetch_info.contract, Some(&platform.config.drive), + PlatformVersion::get(14).expect("expected platform version 14"), ) .expect("expected a document query"); let query_results = platform diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs index 0c8e6789653..384aa8781fe 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v0/mod.rs @@ -221,7 +221,7 @@ pub(super) fn create_domain_data_trigger_v0( internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: BTreeMap::from([ ( @@ -322,7 +322,7 @@ pub(super) fn create_domain_data_trigger_v0( internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: BTreeMap::from([( "saltedDomainHash".to_string(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs index 388db1379a5..06fec8661c2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/dpns/v1/mod.rs @@ -214,7 +214,7 @@ pub(super) fn create_domain_data_trigger_v1( internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: BTreeMap::from([ ( @@ -337,7 +337,7 @@ pub(super) fn create_domain_data_trigger_v1( internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: BTreeMap::from([( "saltedDomainHash".to_string(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs index 32115dcd341..ccf7a81837f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs @@ -67,7 +67,7 @@ pub(super) fn delete_withdrawal_data_trigger_v0( operator: WhereOperator::Equal, value: Value::Identifier(dt_delete.base().id().to_buffer()), }), - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: BTreeMap::default(), }, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs index 5a70fcd1446..66c14377bdc 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v1/mod.rs @@ -61,7 +61,7 @@ pub(super) fn delete_withdrawal_data_trigger_v1( operator: WhereOperator::Equal, value: Value::Identifier(dt_delete.base().id().to_buffer()), }), - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: BTreeMap::default(), }, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs index f344047cbd0..8a5b5a7d41b 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/fetch_documents.rs @@ -112,7 +112,7 @@ fn fetch_documents_for_transitions_knowing_contract_and_document_type_v0( value: Value::Array(ids), }), primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: Default::default(), }, @@ -172,7 +172,7 @@ fn fetch_documents_for_transitions_knowing_contract_and_document_type_v1( value: Value::Array(ids), }), primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: Default::default(), }, @@ -297,7 +297,7 @@ fn fetch_document_with_id_v0( operator: WhereOperator::Equal, value: Value::Identifier(id.to_buffer()), }), - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: Default::default(), }, @@ -359,7 +359,7 @@ fn fetch_document_with_id_v1( operator: WhereOperator::Equal, value: Value::Identifier(id.to_buffer()), }), - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: Default::default(), }, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs index bf4cd507127..7a94babf225 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/dpns.rs @@ -442,7 +442,7 @@ mod dpns_tests { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: Some(WhereClause { field: "records.identity".to_string(), operator: WhereOperator::LessThanOrEquals, @@ -486,7 +486,7 @@ mod dpns_tests { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: BTreeMap::from([( "records.identity".to_string(), @@ -897,7 +897,7 @@ mod dpns_tests { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: Some(WhereClause { field: "records.identity".to_string(), operator: WhereOperator::LessThanOrEquals, @@ -928,7 +928,7 @@ mod dpns_tests { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: BTreeMap::from([( "records.identity".to_string(), @@ -1161,7 +1161,7 @@ mod dpns_username_transfer_tests { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: BTreeMap::from([( "records.identity".to_string(), @@ -1399,7 +1399,7 @@ mod dpns_username_transfer_tests { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: BTreeMap::from([ ( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs index 02ec456bb21..902a665fdcf 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/nft.rs @@ -92,6 +92,7 @@ mod nft_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -275,6 +276,7 @@ mod nft_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -285,6 +287,7 @@ mod nft_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -552,6 +555,7 @@ mod nft_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -562,6 +566,7 @@ mod nft_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -993,6 +998,7 @@ mod nft_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -1003,6 +1009,7 @@ mod nft_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -1553,6 +1560,7 @@ mod nft_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -1563,6 +1571,7 @@ mod nft_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -2459,6 +2468,7 @@ mod nft_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -2469,6 +2479,7 @@ mod nft_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -2713,6 +2724,7 @@ mod nft_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -2723,6 +2735,7 @@ mod nft_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -3091,6 +3104,7 @@ mod nft_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -3256,6 +3270,7 @@ mod nft_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -3266,6 +3281,7 @@ mod nft_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -3792,6 +3808,7 @@ mod nft_tests { buyer_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -4006,6 +4023,7 @@ mod nft_tests { creator_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -4024,6 +4042,7 @@ mod nft_tests { creator_id_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -4625,6 +4644,7 @@ mod nft_tests { buyer1_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -4642,6 +4662,7 @@ mod nft_tests { buyer2_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs index e0294cd1482..9cf7dacdab0 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/replacement.rs @@ -1402,6 +1402,7 @@ mod replacement_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -1412,6 +1413,7 @@ mod replacement_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -1765,6 +1767,7 @@ mod replacement_tests { receiver_documents_sql_string.as_str(), &dashpay, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -1988,6 +1991,7 @@ mod replacement_tests { receiver_documents_sql_string.as_str(), &dashpay, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -2252,6 +2256,7 @@ mod replacement_tests { receiver_documents_sql_string.as_str(), &dashpay, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -2520,6 +2525,7 @@ mod replacement_tests { receiver_documents_sql_string.as_str(), &dashpay, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs index 74cb2289418..cd03d32a7c5 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/transfer.rs @@ -350,6 +350,7 @@ mod transfer_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -360,6 +361,7 @@ mod transfer_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -604,6 +606,7 @@ mod transfer_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -614,6 +617,7 @@ mod transfer_tests { creator_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -624,6 +628,7 @@ mod transfer_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -918,6 +923,7 @@ mod transfer_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -928,6 +934,7 @@ mod transfer_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -1169,6 +1176,7 @@ mod transfer_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -1179,6 +1187,7 @@ mod transfer_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -1332,6 +1341,7 @@ mod transfer_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -1342,6 +1352,7 @@ mod transfer_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -1573,6 +1584,7 @@ mod transfer_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -1583,6 +1595,7 @@ mod transfer_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -1876,6 +1889,7 @@ mod transfer_tests { sender_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -1886,6 +1900,7 @@ mod transfer_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -2260,6 +2275,7 @@ mod transfer_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -2474,6 +2490,7 @@ mod transfer_tests { creator_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -2492,6 +2509,7 @@ mod transfer_tests { creator_id_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -3049,6 +3067,7 @@ mod transfer_tests { creator_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); @@ -3066,6 +3085,7 @@ mod transfer_tests { receiver_documents_sql_string.as_str(), &contract, Some(&platform.config.drive), + platform_version, ) .expect("expected document query"); diff --git a/packages/rs-drive-abci/src/query/document_query/v0/mod.rs b/packages/rs-drive-abci/src/query/document_query/v0/mod.rs index d78b050147d..2bd36c1fad1 100644 --- a/packages/rs-drive-abci/src/query/document_query/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/document_query/v0/mod.rs @@ -244,6 +244,7 @@ impl Platform { contract_ref, document_type, &self.config.drive, + platform_version, )); let response = if prove { @@ -931,7 +932,7 @@ mod tests { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: BTreeMap::from([ ( @@ -1097,7 +1098,7 @@ mod tests { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: BTreeMap::from([ ( @@ -1257,7 +1258,7 @@ mod tests { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: Some(WhereClause { + in_clauses: vec![WhereClause { field: "status".to_string(), operator: WhereOperator::In, value: Value::Array(vec![ @@ -1267,7 +1268,7 @@ mod tests { Value::U8(3), Value::U8(4), ]), - }), + }], range_clause: None, equal_clauses: BTreeMap::default(), }, @@ -1305,8 +1306,9 @@ mod tests { 0, drive_document_query .internal_clauses - .in_clause - .clone() + .in_clauses + .first() + .cloned() .unwrap(), ); @@ -1421,7 +1423,7 @@ mod tests { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: Some(WhereClause { + in_clauses: vec![WhereClause { field: "status".to_string(), operator: WhereOperator::In, value: Value::Array(vec![ @@ -1431,7 +1433,7 @@ mod tests { Value::I64(3), Value::I64(4), ]), - }), + }], range_clause: None, equal_clauses: BTreeMap::default(), }, @@ -1469,8 +1471,9 @@ mod tests { 0, drive_document_query .internal_clauses - .in_clause - .clone() + .in_clauses + .first() + .cloned() .unwrap(), ); @@ -1594,7 +1597,7 @@ mod tests { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: Some(WhereClause { + in_clauses: vec![WhereClause { field: "status".to_string(), operator: WhereOperator::In, value: Value::Array(vec![ @@ -1604,7 +1607,7 @@ mod tests { Value::I64(3), Value::I64(4), ]), - }), + }], range_clause: None, equal_clauses: BTreeMap::from([ ( @@ -1650,8 +1653,9 @@ mod tests { 0, drive_document_query .internal_clauses - .in_clause - .clone() + .in_clauses + .first() + .cloned() .unwrap(), ); diff --git a/packages/rs-drive-abci/src/query/document_query/v1/tests.rs b/packages/rs-drive-abci/src/query/document_query/v1/tests.rs index b3aeb70b843..860c394c18f 100644 --- a/packages/rs-drive-abci/src/query/document_query/v1/tests.rs +++ b/packages/rs-drive-abci/src/query/document_query/v1/tests.rs @@ -1106,7 +1106,7 @@ mod ported_v0_count_tests { .data_contract_owned() } - fn store_person_document( + pub(super) fn store_person_document( platform: &crate::test::helpers::setup::TempPlatform, data_contract: &dpp::prelude::DataContract, id: [u8; 32], @@ -3032,3 +3032,141 @@ mod ranked_tests { } } } + +/// Wire-level coverage for multiple `In` clauses on a compound index +/// (protocol version 14+): the v1 documents select accepts them at +/// v14 on both the no-proof and prove paths, while a protocol +/// version 13 execution of the very same request rejects them with +/// `MultipleInClauses`. +mod multi_in_wire_tests { + use super::ported_v0_count_tests::store_person_document; + use super::*; + use dpp::platform_value::platform_value; + use dpp::tests::json_document::json_document_to_contract_with_ids; + + fn documents_v1_request(data_contract_id: Vec, prove: bool) -> GetDocumentsRequestV1 { + GetDocumentsRequestV1 { + data_contract_id, + document_type: "person".to_string(), + where_clauses: vec![ + wc( + "firstName", + ProtoWhereOperator::In, + platform_value!(["Alice", "Carol", "Eve"]), + ), + wc( + "lastName", + ProtoWhereOperator::In, + platform_value!(["Kriskov", "Smith"]), + ), + ], + order_by: vec![oc("firstName", true), oc("lastName", true)], + limit: None, + start: None, + prove, + selects: select_documents(), + group_by: Vec::new(), + having: Vec::new(), + offset: None, + } + } + + fn setup_people() -> ( + crate::test::helpers::setup::TempPlatform, + std::sync::Arc, + &'static PlatformVersion, + dpp::prelude::DataContract, + ) { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); + + let data_contract = json_document_to_contract_with_ids( + "tests/supporting_files/contract/family/family-contract-countable.json", + None, + None, + false, + version, + ) + .expect("expected to get json based contract"); + store_data_contract(&platform, &data_contract, version); + + for (id, first_name, last_name) in [ + ([1u8; 32], "Alice", "Kriskov"), + ([2u8; 32], "Alice", "Smith"), + ([3u8; 32], "Bob", "Kriskov"), + ([4u8; 32], "Carol", "Smith"), + ([5u8; 32], "Eve", "Sojka"), + ] { + store_person_document( + &platform, + &data_contract, + id, + first_name, + last_name, + 30, + version, + ); + } + + (platform, state, version, data_contract) + } + + #[test] + fn e2e_multiple_in_clauses_documents_select_at_v14() { + let (platform, state, version, data_contract) = setup_people(); + assert!( + version.protocol_version >= 14, + "test platform should run at protocol version 14 or later" + ); + + let request = documents_v1_request(data_contract.id().to_vec(), false); + let result = platform + .query_documents_v1(request, &state, version) + .expect("query call should not error at the transport layer"); + assert!(result.errors.is_empty(), "errors: {:?}", result.errors); + let documents = match result.data.expect("data").result { + Some(get_documents_response_v1::Result::Data(ResultData { + variant: Some(result_data::Variant::Documents(documents)), + })) => documents.documents, + other => panic!("expected documents, got {:?}", other), + }; + // Alice/Kriskov, Alice/Smith, Carol/Smith — Bob and Eve/Sojka + // fall outside the cross product + assert_eq!(documents.len(), 3); + + let request = documents_v1_request(data_contract.id().to_vec(), true); + let result = platform + .query_documents_v1(request, &state, version) + .expect("query call should not error at the transport layer"); + assert!(result.errors.is_empty(), "errors: {:?}", result.errors); + match result.data.expect("data").result { + Some(get_documents_response_v1::Result::Proof(proof)) => { + assert!(!proof.grovedb_proof.is_empty(), "proof should not be empty"); + } + other => panic!("expected proof, got {:?}", other), + } + } + + #[test] + fn e2e_multiple_in_clauses_rejected_at_protocol_version_13() { + let (platform, state, _, data_contract) = setup_people(); + let version_13 = PlatformVersion::get(13).expect("protocol version 13 should exist"); + + let request = documents_v1_request(data_contract.id().to_vec(), false); + let result = platform + .query_documents_v1(request, &state, version_13) + .expect("query call should not error at the transport layer"); + assert!( + !result.errors.is_empty(), + "expected a validation error at protocol version 13" + ); + // The v0 (protocol version 13) grammar rejects the shape at parse + // time, so the error surfaces through the parse wrapping — the + // same wire shape historical nodes produced. + match &result.errors[0] { + QueryError::Drive(drive::error::Error::Query(QuerySyntaxError::MultipleInClauses( + _, + ))) => {} + other => panic!("expected MultipleInClauses, got {:?}", other), + } + } +} diff --git a/packages/rs-drive/SECONDARY_INDEX_QUERIES.md b/packages/rs-drive/SECONDARY_INDEX_QUERIES.md index 3aeb33cba06..81122a9b670 100644 --- a/packages/rs-drive/SECONDARY_INDEX_QUERIES.md +++ b/packages/rs-drive/SECONDARY_INDEX_QUERIES.md @@ -32,7 +32,7 @@ The `InternalClauses` struct organizes WHERE clauses into categories: pub struct InternalClauses { pub primary_key_in_clause: Option, // IN clause on $id pub primary_key_equal_clause: Option, // == clause on $id - pub in_clause: Option, // Single IN clause on indexed field + pub in_clauses: Vec, // Non-primary-key IN clauses (several allowed from protocol version 14 in plain document queries) pub range_clause: Option, // Single range clause (may be combined from 2) pub equal_clauses: BTreeMap, // Multiple == clauses } @@ -238,12 +238,20 @@ This creates the path: `/category=electronics/brand=Apple/price=[500,2000]/` - Maximum: 100 values - No duplicate values - **Range clauses**: Maximum 2 on the same field (combined into Between) -- **One IN clause per query**: Cannot have multiple IN clauses +- **Non-primary-key IN clauses per query**: One before protocol version 14. + From protocol version 14, plain document queries (SELECT documents) allow + several IN clauses when they sit on consecutive properties of one compound + index (after any equality clauses, with an optional range clause right + after the last IN). Each IN'd property needs an `orderBy` entry, the + product of the IN list sizes is capped at 100, and `startAt`/`startAfter` + cannot be combined with more than one IN. Grouped aggregate queries + (count/sum/average/ranked) keep rejecting multiple IN clauses at every + protocol version. - **Field overlap**: Same field cannot appear in different clause types ### 4. Invalid Query Examples -#### Multiple IN clauses (INVALID) +#### Multiple IN clauses (INVALID before protocol version 14) ```json { "where": [ @@ -252,6 +260,8 @@ This creates the path: `/category=electronics/brand=Apple/price=[500,2000]/` ] } ``` +Valid from protocol version 14 when `category` and `status` are consecutive +properties of one compound index and both appear in `orderBy`. #### Range on multiple fields (INVALID) ```json diff --git a/packages/rs-drive/benches/document_count_worst_case.rs b/packages/rs-drive/benches/document_count_worst_case.rs index 801d85ee447..2a00dd884c4 100644 --- a/packages/rs-drive/benches/document_count_worst_case.rs +++ b/packages/rs-drive/benches/document_count_worst_case.rs @@ -2268,7 +2268,7 @@ fn count_request<'a>( // bench keeps its compact fixture vocabulary while the // dispatcher consumes the same typed form the v1 ABCI handler // produces. - let where_clauses = where_clauses_from_value(&raw_where_value) + let where_clauses = where_clauses_from_value(&raw_where_value, PlatformVersion::latest()) .expect("bench fixture builds a valid `where` shape"); let order_clauses = order_clauses_from_value(&raw_order_by_value) .expect("bench fixture builds a valid `order_by` shape"); diff --git a/packages/rs-drive/benches/document_sum_worst_case.rs b/packages/rs-drive/benches/document_sum_worst_case.rs index c7655915cdc..e1953d40b2e 100644 --- a/packages/rs-drive/benches/document_sum_worst_case.rs +++ b/packages/rs-drive/benches/document_sum_worst_case.rs @@ -1898,7 +1898,7 @@ fn sum_request<'a>( // bench keeps its compact fixture vocabulary while the // dispatcher consumes the same typed form the v1 ABCI handler // produces. - let where_clauses = where_clauses_from_value(&raw_where_value) + let where_clauses = where_clauses_from_value(&raw_where_value, PlatformVersion::latest()) .expect("bench fixture builds a valid `where` shape"); let order_clauses = order_clauses_from_value(&raw_order_by_value) .expect("bench fixture builds a valid `order_by` shape"); diff --git a/packages/rs-drive/src/drive/document/delete/mod.rs b/packages/rs-drive/src/drive/document/delete/mod.rs index 398f12e8eb4..9e074da93b2 100644 --- a/packages/rs-drive/src/drive/document/delete/mod.rs +++ b/packages/rs-drive/src/drive/document/delete/mod.rs @@ -145,9 +145,13 @@ mod tests { let sql_string = "select * from person where firstName = 'Samuel' order by firstName asc limit 100"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let (results_no_transaction, _, _) = query .execute_raw_results_no_proof(&drive, None, None, platform_version) @@ -281,9 +285,13 @@ mod tests { let sql_string = "select * from person where firstName = 'Samuel' order by firstName asc limit 100"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let (results_no_transaction, _, _) = query .execute_raw_results_no_proof(&drive, None, None, platform_version) @@ -460,9 +468,13 @@ mod tests { let sql_string = "select * from person where firstName > 'A' order by firstName asc limit 5"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let (results_no_transaction, _, _) = query .execute_raw_results_no_proof(&drive, None, None, platform_version) @@ -500,9 +512,13 @@ mod tests { let sql_string = "select * from person where firstName > 'A' order by firstName asc limit 5"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let (results_no_transaction, _, _) = query .execute_raw_results_no_proof(&drive, None, None, platform_version) @@ -540,9 +556,13 @@ mod tests { let sql_string = "select * from person where firstName > 'A' order by firstName asc limit 5"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let (results_no_transaction, _, _) = query .execute_raw_results_no_proof(&drive, None, None, platform_version) @@ -643,9 +663,13 @@ mod tests { let sql_string = "select * from person where firstName > 'A' order by firstName asc limit 5"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let (results_no_transaction, _, _) = query .execute_raw_results_no_proof(&drive, None, None, platform_version) @@ -757,9 +781,13 @@ mod tests { let sql_string = "select * from person where firstName > 'A' order by firstName asc limit 5"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let (results_no_transaction, _, _) = query .execute_raw_results_no_proof(&drive, None, None, platform_version) @@ -1084,9 +1112,13 @@ mod tests { // Verify the document exists let sql_string = "select * from person where firstName = 'Samuel' order by firstName asc limit 100"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let (results, _, _) = query .execute_raw_results_no_proof(&drive, None, None, platform_version) @@ -1208,9 +1240,13 @@ mod tests { // Verify the document still exists after dry-run delete (apply=false) let sql_string = "select * from person where firstName = 'Samuel' order by firstName asc limit 100"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let (results, _, _) = query .execute_raw_results_no_proof(&drive, None, None, platform_version) diff --git a/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v0/mod.rs b/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v0/mod.rs index cf04062ee65..7612dab7976 100644 --- a/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v0/mod.rs @@ -175,7 +175,7 @@ impl Drive { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: where_queries, }, diff --git a/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v1/mod.rs b/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v1/mod.rs index c9c4b80eb0f..4b5ea32f058 100644 --- a/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v1/mod.rs +++ b/packages/rs-drive/src/drive/document/index_uniqueness/internal/validate_uniqueness_of_data/v1/mod.rs @@ -358,7 +358,7 @@ impl Drive { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: where_queries, }, diff --git a/packages/rs-drive/src/drive/document/insert/mod.rs b/packages/rs-drive/src/drive/document/insert/mod.rs index 6c975495600..b02044b4781 100644 --- a/packages/rs-drive/src/drive/document/insert/mod.rs +++ b/packages/rs-drive/src/drive/document/insert/mod.rs @@ -1014,9 +1014,13 @@ mod tests { // Fetch the document back and verify content matches let sql_string = "select * from profile"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let (results, _, _) = query .execute_raw_results_no_proof(&drive, None, Some(&db_transaction), platform_version) @@ -1133,9 +1137,13 @@ mod tests { // Fetch both documents back and verify they exist with correct content let sql_string = "select * from person order by firstName asc limit 100"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let (results, _, _) = query .execute_raw_results_no_proof(&drive, None, None, platform_version) @@ -1320,9 +1328,13 @@ mod tests { // Verify both documents were inserted by fetching them let sql_string = "select * from contactRequest"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let (results, _, _) = query .execute_raw_results_no_proof(&drive, None, Some(&db_transaction), platform_version) diff --git a/packages/rs-drive/src/drive/document/query/mod.rs b/packages/rs-drive/src/drive/document/query/mod.rs index 1b56d8dc286..5c90d67c59c 100644 --- a/packages/rs-drive/src/drive/document/query/mod.rs +++ b/packages/rs-drive/src/drive/document/query/mod.rs @@ -256,8 +256,13 @@ impl Drive { protocol_version: Option, ) -> Result, Error> { let platform_version = PlatformVersion::get_version_or_current_or_latest(protocol_version)?; - let query = - DriveDocumentQuery::from_cbor(query_cbor, contract, document_type, &self.config)?; + let query = DriveDocumentQuery::from_cbor( + query_cbor, + contract, + document_type, + &self.config, + platform_version, + )?; query.execute_with_proof_internal(self, transaction, drive_operations, platform_version) } @@ -313,8 +318,13 @@ impl Drive { protocol_version: Option, ) -> Result<(RootHash, Vec>), Error> { let platform_version = PlatformVersion::get_version_or_current_or_latest(protocol_version)?; - let query = - DriveDocumentQuery::from_cbor(query_cbor, contract, document_type, &self.config)?; + let query = DriveDocumentQuery::from_cbor( + query_cbor, + contract, + document_type, + &self.config, + platform_version, + )?; query.execute_with_proof_only_get_elements_internal( self, @@ -395,6 +405,7 @@ impl Drive { &contract.contract, document_type, &self.config, + platform_version, )?; self.query_serialized_documents(query, epoch, transaction, platform_version) @@ -412,8 +423,13 @@ impl Drive { protocol_version: Option, ) -> Result<(Vec>, u16), Error> { let platform_version = PlatformVersion::get_version_or_current_or_latest(protocol_version)?; - let query = - DriveDocumentQuery::from_cbor(query_cbor, contract, document_type, &self.config)?; + let query = DriveDocumentQuery::from_cbor( + query_cbor, + contract, + document_type, + &self.config, + platform_version, + )?; query.execute_raw_results_no_proof_internal( self, @@ -526,11 +542,16 @@ mod tests { #[test] fn test_query_documents_dry_run() { let (drive, contract) = setup_dashpay("query-dry-run", true); + let platform_version = PlatformVersion::latest(); let sql_string = "select * from contactRequest"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); // Dry run should return empty results without touching storage let outcome = drive @@ -585,9 +606,13 @@ mod tests { .expect("expected to insert a document successfully"); let sql_string = "select * from contactRequest"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); // Query with epoch to exercise the fee calculation path let epoch = Epoch::new(0).unwrap(); @@ -642,9 +667,13 @@ mod tests { .expect("expected to insert a document successfully"); let sql_string = "select * from contactRequest"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); // Query without epoch, cost should be 0 let outcome = drive @@ -716,9 +745,13 @@ mod tests { let sql_string = "select * from person where firstName = 'Samuel' order by firstName asc limit 100"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); // Query documents with flags to cover the query_documents_with_flags path let outcome = drive @@ -735,11 +768,16 @@ mod tests { #[test] fn test_query_documents_with_flags_dry_run() { let (drive, contract) = setup_dashpay("query-flags-dry", true); + let platform_version = PlatformVersion::latest(); let sql_string = "select * from contactRequest"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); // Dry run should return empty defaults let outcome = drive @@ -812,9 +850,13 @@ mod tests { let sql_string = "select * from person where firstName = 'Samuel' order by firstName asc limit 100"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let epoch = Epoch::new(0).unwrap(); let outcome = drive diff --git a/packages/rs-drive/src/drive/document/query/query_documents/v0/mod.rs b/packages/rs-drive/src/drive/document/query/query_documents/v0/mod.rs index 2ace32531b9..b5ff5d9ad5d 100644 --- a/packages/rs-drive/src/drive/document/query/query_documents/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/query/query_documents/v0/mod.rs @@ -140,9 +140,13 @@ mod tests { let platform_version = PlatformVersion::latest(); let sql = "select * from contactRequest"; - let query = - DriveDocumentQuery::from_sql_expr(sql, &contract, Some(&DriveConfig::default())) - .expect("valid query"); + let query = DriveDocumentQuery::from_sql_expr( + sql, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("valid query"); let outcome = drive .query_documents_v0(query, None, true, None, platform_version) @@ -161,9 +165,13 @@ mod tests { let platform_version = PlatformVersion::latest(); let sql = "select * from contactRequest"; - let query = - DriveDocumentQuery::from_sql_expr(sql, &contract, Some(&DriveConfig::default())) - .expect("valid query"); + let query = DriveDocumentQuery::from_sql_expr( + sql, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("valid query"); let outcome = drive .query_documents_v0(query, None, false, None, platform_version) @@ -234,9 +242,13 @@ mod tests { .expect("insert"); let sql = "select * from contactRequest"; - let query = - DriveDocumentQuery::from_sql_expr(sql, &contract, Some(&DriveConfig::default())) - .expect("valid query"); + let query = DriveDocumentQuery::from_sql_expr( + sql, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("valid query"); let outcome = drive .query_documents_v0(query, None, false, None, platform_version) diff --git a/packages/rs-drive/src/drive/document/query/query_documents_with_flags/v0/mod.rs b/packages/rs-drive/src/drive/document/query/query_documents_with_flags/v0/mod.rs index 5bac26b6003..37c3247ba21 100644 --- a/packages/rs-drive/src/drive/document/query/query_documents_with_flags/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/query/query_documents_with_flags/v0/mod.rs @@ -138,9 +138,13 @@ mod tests { let platform_version = PlatformVersion::latest(); let sql = "select * from contactRequest"; - let query = - DriveDocumentQuery::from_sql_expr(sql, &contract, Some(&DriveConfig::default())) - .expect("valid query"); + let query = DriveDocumentQuery::from_sql_expr( + sql, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("valid query"); let outcome = drive .query_documents_with_flags_v0(query, None, true, None, platform_version) @@ -159,9 +163,13 @@ mod tests { let platform_version = PlatformVersion::latest(); let sql = "select * from contactRequest"; - let query = - DriveDocumentQuery::from_sql_expr(sql, &contract, Some(&DriveConfig::default())) - .expect("valid query"); + let query = DriveDocumentQuery::from_sql_expr( + sql, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("valid query"); let outcome = drive .query_documents_with_flags_v0(query, None, false, None, platform_version) diff --git a/packages/rs-drive/src/drive/document/update/mod.rs b/packages/rs-drive/src/drive/document/update/mod.rs index 9ffa42c3eb2..9b223c9bdf2 100644 --- a/packages/rs-drive/src/drive/document/update/mod.rs +++ b/packages/rs-drive/src/drive/document/update/mod.rs @@ -224,9 +224,13 @@ mod tests { // Check Alice profile let sql_string = "select * from profile"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let (results_no_transaction, _, _) = query .execute_raw_results_no_proof(&drive, None, None, platform_version) @@ -376,9 +380,13 @@ mod tests { // Check Alice profile let sql_string = "select * from profile"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let (results_no_transaction, _, _) = query .execute_raw_results_no_proof(&drive, None, None, platform_version) @@ -489,9 +497,13 @@ mod tests { // Check Alice profile let sql_string = "select * from profile"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let (results_no_transaction, _, _) = query .execute_raw_results_no_proof(&drive, None, None, platform_version) @@ -2297,9 +2309,13 @@ mod tests { // Fetch the document back and verify the update took effect let sql_string = "select * from profile"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let outcome = drive .query_documents(query, None, false, None, None) @@ -2399,9 +2415,13 @@ mod tests { // Fetch the document back and verify the update took effect let sql_string = "select * from profile"; - let query = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); let outcome = drive .query_documents(query, None, false, None, None) diff --git a/packages/rs-drive/src/drive/identity/withdrawals/document/fetch_oldest_withdrawal_documents_by_status/v0/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/document/fetch_oldest_withdrawal_documents_by_status/v0/mod.rs index 0daec1119f7..e7aada7b4d8 100644 --- a/packages/rs-drive/src/drive/identity/withdrawals/document/fetch_oldest_withdrawal_documents_by_status/v0/mod.rs +++ b/packages/rs-drive/src/drive/identity/withdrawals/document/fetch_oldest_withdrawal_documents_by_status/v0/mod.rs @@ -55,7 +55,7 @@ impl Drive { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: where_clauses, }, @@ -101,7 +101,7 @@ impl Drive { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: where_clauses, }, diff --git a/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/mod.rs index ca67af28bc7..4014f99a77d 100644 --- a/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/mod.rs +++ b/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/mod.rs @@ -1,4 +1,5 @@ mod v0; +mod v1; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -34,9 +35,16 @@ impl Drive { transaction, platform_version, ), + 1 => self.find_withdrawal_documents_by_status_and_transaction_indices_v1( + status, + transaction_indices, + limit, + transaction, + platform_version, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "find_withdrawal_documents_by_status_and_transaction_indices".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v0/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v0/mod.rs index 170d3a98fa9..95633f0b2e9 100644 --- a/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v0/mod.rs +++ b/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v0/mod.rs @@ -74,7 +74,7 @@ impl Drive { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: where_clauses, }, @@ -165,16 +165,22 @@ mod tests { Some(&transaction), ); - let found_document = drive - .find_withdrawal_documents_by_status_and_transaction_indices( - withdrawals_contract::WithdrawalStatus::POOLED, - &[transaction_index], - DEFAULT_QUERY_LIMIT, - Some(&transaction), - platform_version, - ) - .expect("to find document by it's transaction id"); - - assert_eq!(found_document.len(), 1); + // Protocol version 13 routes to v0, 14 to v1 — both must find the + // same document (the two builders lower identically) + for protocol_version in [13u32, 14u32] { + let version = + PlatformVersion::get(protocol_version).expect("expected platform version to exist"); + let found_document = drive + .find_withdrawal_documents_by_status_and_transaction_indices( + withdrawals_contract::WithdrawalStatus::POOLED, + &[transaction_index], + DEFAULT_QUERY_LIMIT, + Some(&transaction), + version, + ) + .expect("to find document by it's transaction id"); + + assert_eq!(found_document.len(), 1); + } } } diff --git a/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v1/mod.rs b/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v1/mod.rs new file mode 100644 index 00000000000..64e84b84754 --- /dev/null +++ b/packages/rs-drive/src/drive/identity/withdrawals/document/find_withdrawal_documents_by_status_and_transaction_indices/v1/mod.rs @@ -0,0 +1,103 @@ +use crate::drive::document::query::QueryDocumentsOutcomeV0Methods; +use crate::drive::Drive; +use crate::error::Error; +use crate::query::{DriveDocumentQuery, InternalClauses, OrderClause, WhereClause}; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contracts::withdrawals_contract; +use dpp::data_contracts::withdrawals_contract::v1::document_types::withdrawal; +use dpp::document::Document; +use dpp::platform_value::Value; +use dpp::withdrawal::WithdrawalTransactionIndex; +use grovedb::TransactionArg; +use indexmap::IndexMap; +use platform_version::version::PlatformVersion; +use std::collections::BTreeMap; + +impl Drive { + // v1 (protocol version 14): identical to v0 except the transaction-index + // `In` clause is carried in `InternalClauses.in_clauses` instead of + // riding in `equal_clauses`. Both shapes lower to the identical grovedb + // path query (pinned by `withdrawal_in_clause_placement_equivalence` + // in the query tests); this is a structural cleanup version. + + // TODO(withdrawals): Currently it queries only up to 100 documents. + // It works while we don't have pooling + // This should be a pathquery directly instead of a drive query for efficiency + + pub(super) fn find_withdrawal_documents_by_status_and_transaction_indices_v1( + &self, + status: withdrawals_contract::WithdrawalStatus, + transaction_indices: &[WithdrawalTransactionIndex], + limit: u16, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let mut equal_clauses = BTreeMap::new(); + + equal_clauses.insert( + withdrawal::properties::STATUS.to_string(), + WhereClause { + field: withdrawal::properties::STATUS.to_string(), + operator: crate::query::WhereOperator::Equal, + value: Value::U8(status as u8), + }, + ); + + let transaction_index_in_clause = WhereClause { + field: withdrawal::properties::TRANSACTION_INDEX.to_string(), + operator: crate::query::WhereOperator::In, + value: Value::Array( + transaction_indices + .iter() + .map(|index| Value::U64(*index)) + .collect::>(), + ), + }; + + let mut order_by = IndexMap::new(); + + order_by.insert( + withdrawal::properties::TRANSACTION_INDEX.to_string(), + OrderClause { + field: withdrawal::properties::TRANSACTION_INDEX.to_string(), + ascending: true, + }, + ); + + let contract = self + .cache + .system_data_contracts + .load_withdrawals(platform_version)?; + + let document_type = contract.document_type_for_name(withdrawal::NAME)?; + + let drive_query = DriveDocumentQuery { + contract: &contract, + document_type, + internal_clauses: InternalClauses { + primary_key_in_clause: None, + primary_key_equal_clause: None, + in_clauses: vec![transaction_index_in_clause], + range_clause: None, + equal_clauses, + }, + offset: None, + limit: Some(limit), + order_by, + start_at: None, + start_at_included: false, + block_time_ms: None, + }; + + // todo: deal with cost of this operation + let outcome = self.query_documents( + drive_query, + None, + false, + transaction, + Some(platform_version.protocol_version), + )?; + + Ok(outcome.documents_owned()) + } +} diff --git a/packages/rs-drive/src/query/conditions.rs b/packages/rs-drive/src/query/conditions.rs index ef7f928608a..7fb394f3ef0 100644 --- a/packages/rs-drive/src/query/conditions.rs +++ b/packages/rs-drive/src/query/conditions.rs @@ -16,7 +16,7 @@ use grovedb::Query; use sqlparser::ast; use std::borrow::Cow; use std::cmp::Ordering; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeMap; use std::fmt::Display; use WhereOperator::{ Between, BetweenExcludeBounds, BetweenExcludeLeft, BetweenExcludeRight, Equal, GreaterThan, @@ -524,7 +524,9 @@ impl<'a> WhereClause { }) } - fn lower_bound_clause(where_clauses: &'a [&WhereClause]) -> Result, Error> { + pub(crate) fn lower_bound_clause( + where_clauses: &'a [&WhereClause], + ) -> Result, Error> { let lower_range_clauses: Vec<&&WhereClause> = where_clauses .iter() .filter(|&where_clause| { @@ -540,7 +542,9 @@ impl<'a> WhereClause { } } - fn upper_bound_clause(where_clauses: &'a [&WhereClause]) -> Result, Error> { + pub(crate) fn upper_bound_clause( + where_clauses: &'a [&WhereClause], + ) -> Result, Error> { let upper_range_clauses: Vec<&&WhereClause> = where_clauses .iter() .filter(|&where_clause| matches!(where_clause.operator, LessThan | LessThanOrEquals)) @@ -554,215 +558,22 @@ impl<'a> WhereClause { } } - /// Given a list of where clauses, returns them in groups of equal, range, and in clauses + /// Given a list of where clauses, returns them in groups of equal, range, + /// and in clauses, under the platform version's grammar. + /// + /// Versioned via + /// `platform_version.drive.methods.document.query.where_clause_grouping` + /// (see [`crate::query::where_clause_grouping`]): v0 — every protocol + /// version up to 13 — rejects more than one non-primary-key `In` clause + /// with `MultipleInClauses` before any other same-field checks; v1 + /// (protocol version 14) groups multiple `In` clauses structurally, and + /// their acceptance is decided by the versioned path-query lowering. #[allow(clippy::type_complexity)] pub(crate) fn group_clauses( where_clauses: &'a [WhereClause], - // TODO: Define a type/struct for return value - ) -> Result<(BTreeMap, Option, Option), Error> { - if where_clauses.is_empty() { - return Ok((BTreeMap::new(), None, None)); - } - let equal_clauses_array = - where_clauses - .iter() - .filter_map(|where_clause| match where_clause.operator { - Equal => match where_clause.is_identifier() { - true => None, - false => Some(where_clause.clone()), - }, - _ => None, - }); - let mut known_fields: BTreeSet = BTreeSet::new(); - let equal_clauses: BTreeMap = equal_clauses_array - .into_iter() - .map(|where_clause| { - if known_fields.contains(&where_clause.field) { - Err(Error::Query( - QuerySyntaxError::DuplicateNonGroupableClauseSameField( - "duplicate equality fields", - ), - )) - } else { - known_fields.insert(where_clause.field.clone()); - Ok((where_clause.field.clone(), where_clause)) - } - }) - .collect::, Error>>()?; - - let in_clauses_array = where_clauses - .iter() - .filter_map(|where_clause| match where_clause.operator { - In => match where_clause.is_identifier() { - true => None, - false => Some(where_clause.clone()), - }, - _ => None, - }) - .collect::>(); - - let in_clause = match in_clauses_array.len() { - 0 => Ok(None), - 1 => { - let clause = in_clauses_array.first().expect("there must be a value"); - if known_fields.contains(&clause.field) { - Err(Error::Query( - QuerySyntaxError::DuplicateNonGroupableClauseSameField( - "in clause has same field as an equality clause", - ), - )) - } else { - known_fields.insert(clause.field.clone()); - Ok(Some(clause.clone())) - } - } - _ => Err(Error::Query(QuerySyntaxError::MultipleInClauses( - "There should only be one in clause", - ))), - }?; - - // In order to group range clauses - let groupable_range_clauses: Vec<&WhereClause> = where_clauses - .iter() - .filter(|where_clause| match where_clause.operator { - Equal => false, - In => false, - GreaterThan => true, - GreaterThanOrEquals => true, - LessThan => true, - LessThanOrEquals => true, - StartsWith => false, - Between => false, - BetweenExcludeBounds => false, - BetweenExcludeRight => false, - BetweenExcludeLeft => false, - }) - .collect(); - - let non_groupable_range_clauses: Vec<&WhereClause> = where_clauses - .iter() - .filter(|where_clause| match where_clause.operator { - Equal => false, - In => false, - GreaterThan => false, - GreaterThanOrEquals => false, - LessThan => false, - LessThanOrEquals => false, - StartsWith => true, - Between => true, - BetweenExcludeBounds => true, - BetweenExcludeRight => true, - BetweenExcludeLeft => true, - }) - .collect(); - - let range_clause = - if non_groupable_range_clauses.is_empty() { - if groupable_range_clauses.is_empty() { - Ok(None) - } else if groupable_range_clauses.len() == 1 { - let clause = *groupable_range_clauses.first().unwrap(); - if known_fields.contains(clause.field.as_str()) { - Err(Error::Query( - QuerySyntaxError::InvalidWhereClauseComponents( - "in clause has same field as an equality clause", - ), - )) - } else { - Ok(Some(clause.clone())) - } - } else if groupable_range_clauses.len() > 2 { - Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( - "there can only be at most 2 range clauses that must be on the same field", - ))) - } else { - let first_field = groupable_range_clauses.first().unwrap().field.as_str(); - if known_fields.contains(first_field) { - Err(Error::Query( - QuerySyntaxError::InvalidWhereClauseComponents( - "a range clause has same field as an equality or in clause", - ), - )) - } else if groupable_range_clauses - .iter() - .any(|&z| z.field.as_str() != first_field) - { - Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( - "all ranges must be on same field", - ))) - } else { - let lower_upper_error = || { - Error::Query(QuerySyntaxError::RangeClausesNotGroupable( - "lower and upper bounds must be passed if providing 2 ranges", - )) - }; - - // we need to find the bounds of the clauses - let lower_bounds_clause = - WhereClause::lower_bound_clause(groupable_range_clauses.as_slice())? - .ok_or_else(lower_upper_error)?; - let upper_bounds_clause = - WhereClause::upper_bound_clause(groupable_range_clauses.as_slice())? - .ok_or_else(lower_upper_error)?; - - let operator = - match (lower_bounds_clause.operator, upper_bounds_clause.operator) { - (GreaterThanOrEquals, LessThanOrEquals) => Some(Between), - (GreaterThanOrEquals, LessThan) => Some(BetweenExcludeRight), - (GreaterThan, LessThanOrEquals) => Some(BetweenExcludeLeft), - (GreaterThan, LessThan) => Some(BetweenExcludeBounds), - _ => None, - } - .ok_or_else(lower_upper_error)?; - - if upper_bounds_clause - .less_than(lower_bounds_clause, operator == BetweenExcludeBounds)? - { - return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( - "lower bounds must be under upper bounds", - ))); - } - - Ok(Some(WhereClause { - field: groupable_range_clauses.first().unwrap().field.clone(), - operator, - value: Value::Array(vec![ - lower_bounds_clause.value.clone(), - upper_bounds_clause.value.clone(), - ]), - })) - } - } - } else if non_groupable_range_clauses.len() == 1 && groupable_range_clauses.is_empty() { - let where_clause = *non_groupable_range_clauses.first().unwrap(); - if where_clause.operator == StartsWith { - // Starts with must null be against an empty string - if let Value::Text(text) = &where_clause.value { - if text.is_empty() { - return Err(Error::Query(QuerySyntaxError::StartsWithIllegalString( - "starts with can not start with an empty string", - ))); - } - } - } - if known_fields.contains(where_clause.field.as_str()) { - Err(Error::Query(QuerySyntaxError::DuplicateNonGroupableClauseSameField( - "a non groupable range clause has same field as an equality or in clause", - ))) - } else { - Ok(Some(where_clause.clone())) - } - } else if groupable_range_clauses.is_empty() { - Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( - "there can not be more than 1 non groupable range clause", - ))) - } else { - Err(Error::Query(QuerySyntaxError::RangeClausesNotGroupable( - "clauses are not groupable", - ))) - }?; - - Ok((equal_clauses, range_clause, in_clause)) + platform_version: &PlatformVersion, + ) -> Result<(BTreeMap, Option, Vec), Error> { + crate::query::where_clause_grouping::group_where_clauses(where_clauses, platform_version) } fn split_value_for_between( @@ -1720,6 +1531,7 @@ mod tests { use dpp::platform_value::Value; use dpp::prelude::Identifier; use dpp::tests::fixtures::get_data_contract_fixture; + use dpp::version::PlatformVersion; use dpp::version::LATEST_PLATFORM_VERSION; use grovedb::Query; use std::collections::BTreeMap; @@ -1818,8 +1630,9 @@ mod tests { value: Value::Float(1.0), }, ]; - let (_, range_clause, _) = WhereClause::group_clauses(&where_clauses) - .expect("expected to have groupable pair"); + let (_, range_clause, _) = + WhereClause::group_clauses(&where_clauses, PlatformVersion::latest()) + .expect("expected to have groupable pair"); range_clause.expect("expected to have range clause returned"); } } @@ -1844,8 +1657,9 @@ mod tests { value: Value::Float(0.0), }, ]; - let (_, range_clause, _) = WhereClause::group_clauses(&where_clauses) - .expect("expected to have groupable pair"); + let (_, range_clause, _) = + WhereClause::group_clauses(&where_clauses, PlatformVersion::latest()) + .expect("expected to have groupable pair"); range_clause.expect("expected to have range clause returned"); } } @@ -1866,7 +1680,7 @@ mod tests { value: Value::Float(1.0), }, ]; - WhereClause::group_clauses(&where_clauses) + WhereClause::group_clauses(&where_clauses, PlatformVersion::latest()) .expect_err("expected to have an error returned"); } } @@ -1885,7 +1699,7 @@ mod tests { value: Value::Float(1.0), }, ]; - WhereClause::group_clauses(&where_clauses) + WhereClause::group_clauses(&where_clauses, PlatformVersion::latest()) .expect_err("different fields should not be groupable"); } @@ -1917,7 +1731,7 @@ mod tests { value: Value::Float(1.0), }, ]; - WhereClause::group_clauses(&where_clauses) + WhereClause::group_clauses(&where_clauses, PlatformVersion::latest()) .expect_err("expected to not have a groupable pair"); } } @@ -3067,10 +2881,11 @@ mod tests { #[test] fn group_clauses_empty_input() { let clauses: Vec = vec![]; - let (eq, range, in_c) = WhereClause::group_clauses(&clauses).expect("empty should succeed"); + let (eq, range, in_c) = WhereClause::group_clauses(&clauses, PlatformVersion::latest()) + .expect("empty should succeed"); assert!(eq.is_empty()); assert!(range.is_none()); - assert!(in_c.is_none()); + assert!(in_c.is_empty()); } #[test] @@ -3080,11 +2895,12 @@ mod tests { operator: Equal, value: Value::Text("alice".to_string()), }]; - let (eq, range, in_c) = WhereClause::group_clauses(&clauses).unwrap(); + let (eq, range, in_c) = + WhereClause::group_clauses(&clauses, PlatformVersion::latest()).unwrap(); assert_eq!(eq.len(), 1); assert!(eq.contains_key("name")); assert!(range.is_none()); - assert!(in_c.is_none()); + assert!(in_c.is_empty()); } #[test] @@ -3094,11 +2910,12 @@ mod tests { operator: Equal, value: Value::I64(1), }]; - let (eq, range, in_c) = WhereClause::group_clauses(&clauses).unwrap(); + let (eq, range, in_c) = + WhereClause::group_clauses(&clauses, PlatformVersion::latest()).unwrap(); // $id equality is excluded from the equal_clauses map assert!(eq.is_empty()); assert!(range.is_none()); - assert!(in_c.is_none()); + assert!(in_c.is_empty()); } #[test] @@ -3108,10 +2925,11 @@ mod tests { operator: In, value: Value::Array(vec![Value::I64(1), Value::I64(2)]), }]; - let (eq, range, in_c) = WhereClause::group_clauses(&clauses).unwrap(); + let (eq, range, in_c) = + WhereClause::group_clauses(&clauses, PlatformVersion::latest()).unwrap(); assert!(eq.is_empty()); assert!(range.is_none()); - assert!(in_c.is_none()); + assert!(in_c.is_empty()); } #[test] @@ -3121,15 +2939,19 @@ mod tests { operator: In, value: Value::Array(vec![Value::I64(1), Value::I64(2)]), }]; - let (eq, range, in_c) = WhereClause::group_clauses(&clauses).unwrap(); + let (eq, range, in_c) = + WhereClause::group_clauses(&clauses, PlatformVersion::latest()).unwrap(); assert!(eq.is_empty()); assert!(range.is_none()); - assert!(in_c.is_some()); - assert_eq!(in_c.unwrap().field, "status"); + assert_eq!(in_c.len(), 1); + assert_eq!(in_c[0].field, "status"); } #[test] - fn group_clauses_multiple_in_returns_error() { + fn group_clauses_multiple_in_on_distinct_fields_groups_structurally() { + // Whether more than one in clause is accepted is decided at + // path-query lowering (protocol version 14+); the grammar groups + // them structurally in query order. let clauses = vec![ WhereClause { field: "a".to_string(), @@ -3142,7 +2964,107 @@ mod tests { value: Value::Array(vec![Value::I64(2)]), }, ]; - assert!(WhereClause::group_clauses(&clauses).is_err()); + let (eq, range, in_c) = + WhereClause::group_clauses(&clauses, PlatformVersion::latest()).unwrap(); + assert!(eq.is_empty()); + assert!(range.is_none()); + assert_eq!(in_c.len(), 2); + assert_eq!(in_c[0].field, "a"); + assert_eq!(in_c[1].field, "b"); + } + + #[test] + fn group_clauses_multiple_in_v13_reports_multiple_in_before_any_other_check() { + // The v0 grammar (protocol versions up to 13) rejects any query + // with more than one non-primary-key in clause with + // MultipleInClauses before duplicate-field, overlap, or range + // checks — the error surface historical nodes committed to. + let platform_version_13 = + PlatformVersion::get(13).expect("protocol version 13 should exist"); + let shapes: Vec> = vec![ + // duplicate in fields + vec![ + WhereClause { + field: "a".to_string(), + operator: In, + value: Value::Array(vec![Value::I64(1)]), + }, + WhereClause { + field: "a".to_string(), + operator: In, + value: Value::Array(vec![Value::I64(2)]), + }, + ], + // in overlapping an equality + vec![ + WhereClause { + field: "a".to_string(), + operator: Equal, + value: Value::I64(1), + }, + WhereClause { + field: "a".to_string(), + operator: In, + value: Value::Array(vec![Value::I64(2)]), + }, + WhereClause { + field: "b".to_string(), + operator: In, + value: Value::Array(vec![Value::I64(3)]), + }, + ], + // two ranges on different fields next to two ins + vec![ + WhereClause { + field: "a".to_string(), + operator: In, + value: Value::Array(vec![Value::I64(1)]), + }, + WhereClause { + field: "b".to_string(), + operator: In, + value: Value::Array(vec![Value::I64(2)]), + }, + WhereClause { + field: "c".to_string(), + operator: GreaterThan, + value: Value::I64(5), + }, + WhereClause { + field: "d".to_string(), + operator: super::LessThan, + value: Value::I64(3), + }, + ], + ]; + for clauses in shapes { + let error = WhereClause::group_clauses(&clauses, platform_version_13) + .expect_err("multi-in shapes must be rejected at protocol version 13"); + assert!( + matches!( + error, + crate::error::Error::Query(QuerySyntaxError::MultipleInClauses(_)) + ), + "expected MultipleInClauses, got {error:?}" + ); + } + } + + #[test] + fn group_clauses_multiple_in_on_same_field_returns_error() { + let clauses = vec![ + WhereClause { + field: "a".to_string(), + operator: In, + value: Value::Array(vec![Value::I64(1)]), + }, + WhereClause { + field: "a".to_string(), + operator: In, + value: Value::Array(vec![Value::I64(2)]), + }, + ]; + assert!(WhereClause::group_clauses(&clauses, PlatformVersion::latest()).is_err()); } #[test] @@ -3159,7 +3081,7 @@ mod tests { value: Value::Array(vec![Value::I64(2)]), }, ]; - assert!(WhereClause::group_clauses(&clauses).is_err()); + assert!(WhereClause::group_clauses(&clauses, PlatformVersion::latest()).is_err()); } #[test] @@ -3176,7 +3098,7 @@ mod tests { value: Value::Text("bob".to_string()), }, ]; - assert!(WhereClause::group_clauses(&clauses).is_err()); + assert!(WhereClause::group_clauses(&clauses, PlatformVersion::latest()).is_err()); } #[test] @@ -3186,11 +3108,12 @@ mod tests { operator: GreaterThan, value: Value::I64(18), }]; - let (eq, range, in_c) = WhereClause::group_clauses(&clauses).unwrap(); + let (eq, range, in_c) = + WhereClause::group_clauses(&clauses, PlatformVersion::latest()).unwrap(); assert!(eq.is_empty()); assert!(range.is_some()); assert_eq!(range.unwrap().operator, GreaterThan); - assert!(in_c.is_none()); + assert!(in_c.is_empty()); } #[test] @@ -3200,11 +3123,12 @@ mod tests { operator: Between, value: Value::Array(vec![Value::Float(0.0), Value::Float(100.0)]), }]; - let (eq, range, in_c) = WhereClause::group_clauses(&clauses).unwrap(); + let (eq, range, in_c) = + WhereClause::group_clauses(&clauses, PlatformVersion::latest()).unwrap(); assert!(eq.is_empty()); assert!(range.is_some()); assert_eq!(range.unwrap().operator, Between); - assert!(in_c.is_none()); + assert!(in_c.is_empty()); } #[test] @@ -3214,7 +3138,7 @@ mod tests { operator: super::StartsWith, value: Value::Text("".to_string()), }]; - assert!(WhereClause::group_clauses(&clauses).is_err()); + assert!(WhereClause::group_clauses(&clauses, PlatformVersion::latest()).is_err()); } #[test] @@ -3224,11 +3148,12 @@ mod tests { operator: super::StartsWith, value: Value::Text("al".to_string()), }]; - let (eq, range, in_c) = WhereClause::group_clauses(&clauses).unwrap(); + let (eq, range, in_c) = + WhereClause::group_clauses(&clauses, PlatformVersion::latest()).unwrap(); assert!(eq.is_empty()); assert!(range.is_some()); assert_eq!(range.unwrap().operator, super::StartsWith); - assert!(in_c.is_none()); + assert!(in_c.is_empty()); } #[test] @@ -3245,7 +3170,7 @@ mod tests { value: Value::Text("al".to_string()), }, ]; - assert!(WhereClause::group_clauses(&clauses).is_err()); + assert!(WhereClause::group_clauses(&clauses, PlatformVersion::latest()).is_err()); } #[test] @@ -3262,7 +3187,7 @@ mod tests { value: Value::Text("x".to_string()), }, ]; - assert!(WhereClause::group_clauses(&clauses).is_err()); + assert!(WhereClause::group_clauses(&clauses, PlatformVersion::latest()).is_err()); } #[test] @@ -3279,7 +3204,7 @@ mod tests { value: Value::Array(vec![Value::Float(0.0), Value::Float(10.0)]), }, ]; - assert!(WhereClause::group_clauses(&clauses).is_err()); + assert!(WhereClause::group_clauses(&clauses, PlatformVersion::latest()).is_err()); } #[test] @@ -3301,7 +3226,7 @@ mod tests { value: Value::Float(5.0), }, ]; - assert!(WhereClause::group_clauses(&clauses).is_err()); + assert!(WhereClause::group_clauses(&clauses, PlatformVersion::latest()).is_err()); } #[test] @@ -3318,7 +3243,7 @@ mod tests { value: Value::I64(18), }, ]; - assert!(WhereClause::group_clauses(&clauses).is_err()); + assert!(WhereClause::group_clauses(&clauses, PlatformVersion::latest()).is_err()); } #[test] @@ -3335,7 +3260,8 @@ mod tests { value: Value::Float(20.0), }, ]; - let (_, range, _) = WhereClause::group_clauses(&clauses).unwrap(); + let (_, range, _) = + WhereClause::group_clauses(&clauses, PlatformVersion::latest()).unwrap(); let r = range.unwrap(); assert_eq!(r.operator, Between); assert_eq!(r.field, "age"); @@ -3355,7 +3281,8 @@ mod tests { value: Value::Float(20.0), }, ]; - let (_, range, _) = WhereClause::group_clauses(&clauses).unwrap(); + let (_, range, _) = + WhereClause::group_clauses(&clauses, PlatformVersion::latest()).unwrap(); assert_eq!(range.unwrap().operator, BetweenExcludeRight); } @@ -3373,7 +3300,8 @@ mod tests { value: Value::Float(20.0), }, ]; - let (_, range, _) = WhereClause::group_clauses(&clauses).unwrap(); + let (_, range, _) = + WhereClause::group_clauses(&clauses, PlatformVersion::latest()).unwrap(); assert_eq!(range.unwrap().operator, BetweenExcludeLeft); } @@ -3391,7 +3319,8 @@ mod tests { value: Value::Float(20.0), }, ]; - let (_, range, _) = WhereClause::group_clauses(&clauses).unwrap(); + let (_, range, _) = + WhereClause::group_clauses(&clauses, PlatformVersion::latest()).unwrap(); assert_eq!(range.unwrap().operator, BetweenExcludeBounds); } @@ -3409,9 +3338,10 @@ mod tests { value: Value::Array(vec![Value::I64(1), Value::I64(2)]), }, ]; - let (eq, _, in_c) = WhereClause::group_clauses(&clauses).unwrap(); + let (eq, _, in_c) = + WhereClause::group_clauses(&clauses, PlatformVersion::latest()).unwrap(); assert_eq!(eq.len(), 1); - assert!(in_c.is_some()); + assert_eq!(in_c.len(), 1); } #[test] @@ -3428,10 +3358,11 @@ mod tests { value: Value::Float(18.0), }, ]; - let (eq, range, in_c) = WhereClause::group_clauses(&clauses).unwrap(); + let (eq, range, in_c) = + WhereClause::group_clauses(&clauses, PlatformVersion::latest()).unwrap(); assert_eq!(eq.len(), 1); assert!(range.is_some()); - assert!(in_c.is_none()); + assert!(in_c.is_empty()); } // ---- meta_field_property_type ---- diff --git a/packages/rs-drive/src/query/defaults.rs b/packages/rs-drive/src/query/defaults.rs index 9f33f2a14b5..96df2e42811 100644 --- a/packages/rs-drive/src/query/defaults.rs +++ b/packages/rs-drive/src/query/defaults.rs @@ -1,2 +1,8 @@ /// Max index difference constant pub(crate) const MAX_INDEX_DIFFERENCE: u16 = 2; + +/// Maximum product of `In` clause list sizes in a single document query. +/// Each `In` value opens one subtree of the compound index, so this caps the +/// branch enumeration of a multi-`In` query at the same worst case as a +/// single maximal `In` clause (100 values). +pub const MAX_IN_CROSS_PRODUCT_SIZE: usize = 100; diff --git a/packages/rs-drive/src/query/drive_document_count_and_sum_query/drive_dispatcher.rs b/packages/rs-drive/src/query/drive_document_count_and_sum_query/drive_dispatcher.rs index ee0eea90811..1442184ddc7 100644 --- a/packages/rs-drive/src/query/drive_document_count_and_sum_query/drive_dispatcher.rs +++ b/packages/rs-drive/src/query/drive_document_count_and_sum_query/drive_dispatcher.rs @@ -109,7 +109,8 @@ impl Drive { // clause whose presence changes the routing decision. See // [`validate_and_canonicalize_where_clauses`]'s docstring for // the catalog of rejections. - let where_clauses = validate_and_canonicalize_where_clauses(request.where_clauses)?; + let where_clauses = + validate_and_canonicalize_where_clauses(request.where_clauses, platform_version)?; // Convert AverageMode → SumMode (1:1 by construction); sum's // routing table is the single source of truth for the diff --git a/packages/rs-drive/src/query/drive_document_count_query/drive_dispatcher.rs b/packages/rs-drive/src/query/drive_document_count_query/drive_dispatcher.rs index b75bee10b13..6237e3f009a 100644 --- a/packages/rs-drive/src/query/drive_document_count_query/drive_dispatcher.rs +++ b/packages/rs-drive/src/query/drive_document_count_query/drive_dispatcher.rs @@ -142,7 +142,9 @@ pub enum DocumentCountResponse { /// /// - Duplicate `Equal` clauses on the same field /// (`DuplicateNonGroupableClauseSameField`). -/// - Multiple `In` clauses (`MultipleInClauses`). +/// - Multiple `In` clauses (`MultipleInClauses`) — rejected here: the +/// shared grammar accepts them for protocol version 14+ document +/// queries, but the aggregate surfaces do not. /// - Multiple non-groupable range clauses (`MultipleRangeClauses`). /// - Equality + `In` on the same field, range + equality/In on the /// same field (`DuplicateNonGroupableClauseSameField` / @@ -169,6 +171,7 @@ pub enum DocumentCountResponse { /// count path doesn't.) pub fn where_clauses_from_value( value: &dpp::platform_value::Value, + platform_version: &PlatformVersion, ) -> Result, Error> { let clauses: Vec = match value { dpp::platform_value::Value::Null => Vec::new(), @@ -190,7 +193,7 @@ pub fn where_clauses_from_value( } }; - validate_and_canonicalize_where_clauses(clauses) + validate_and_canonicalize_where_clauses(clauses, platform_version) } /// Run the system-wide where-clause validator on a structured @@ -203,7 +206,9 @@ pub fn where_clauses_from_value( /// The validator (`WhereClause::group_clauses`) rejects: /// - Duplicate `Equal` clauses on the same field /// (`DuplicateNonGroupableClauseSameField`). -/// - Multiple `In` clauses (`MultipleInClauses`). +/// - Multiple `In` clauses (`MultipleInClauses`) — rejected here: the +/// shared grammar accepts them for protocol version 14+ document +/// queries, but the aggregate surfaces do not. /// - Multiple non-groupable range clauses (`MultipleRangeClauses`). /// - Equality + `In` on the same field, range + equality/In on the /// same field (`DuplicateNonGroupableClauseSameField` / @@ -246,8 +251,17 @@ pub fn where_clauses_from_value( /// rejected by `detect_mode`'s `range_count > 1` structural check. pub fn validate_and_canonicalize_where_clauses( clauses: Vec, + platform_version: &PlatformVersion, ) -> Result, Error> { - match WhereClause::group_clauses(&clauses) { + match WhereClause::group_clauses(&clauses, platform_version) { + // Multiple `In` clauses are a document-query-only shape (protocol + // version 14+); the aggregate surfaces keep rejecting them since + // their mode detection and index pickers assume a single `In`. + Ok((_, _, in_clauses)) if in_clauses.len() > 1 => { + return Err(Error::Query(QuerySyntaxError::MultipleInClauses( + "aggregate queries support at most one in clause", + ))); + } Ok(_) => {} Err(Error::Query(QuerySyntaxError::MultipleRangeClauses(_))) => {} Err(e) => return Err(e), @@ -429,7 +443,8 @@ impl Drive { // shaped legacy path or the v1 typed-proto path. See // [`validate_and_canonicalize_where_clauses`]'s docstring // for the catalog of rejections / canonicalization rules. - let where_clauses = validate_and_canonicalize_where_clauses(request.where_clauses)?; + let where_clauses = + validate_and_canonicalize_where_clauses(request.where_clauses, platform_version)?; let order_clauses = request.order_clauses; // Split-mode entry direction is whatever the first orderBy diff --git a/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs b/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs index 68ce1b2aad9..11d3ef38db8 100644 --- a/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs +++ b/packages/rs-drive/src/query/drive_document_sum_query/drive_dispatcher.rs @@ -273,8 +273,14 @@ impl Drive { /// Parse the wire-CBOR `Value::Array` shape into structured /// `Vec`. Delegates to count's parser. -pub fn where_clauses_from_value(value: &Value) -> Result, Error> { - crate::query::drive_document_count_query::drive_dispatcher::where_clauses_from_value(value) +pub fn where_clauses_from_value( + value: &Value, + platform_version: &PlatformVersion, +) -> Result, Error> { + crate::query::drive_document_count_query::drive_dispatcher::where_clauses_from_value( + value, + platform_version, + ) } /// Parse the wire-CBOR `Value::Array` shape into structured diff --git a/packages/rs-drive/src/query/filter.rs b/packages/rs-drive/src/query/filter.rs index f3a64713f3e..aaf1ab080fa 100644 --- a/packages/rs-drive/src/query/filter.rs +++ b/packages/rs-drive/src/query/filter.rs @@ -389,8 +389,8 @@ impl DriveDocumentQueryFilter<'_> { } } - // In clause - if let Some(in_clause) = &clauses.in_clause { + // In clauses + for in_clause in &clauses.in_clauses { let field_value = get_value_by_path(document_data, &in_clause.field); if let Some(value) = field_value { if !in_clause.matches_value(value) { @@ -770,11 +770,11 @@ mod tests { ]; let internal_clauses = InternalClauses { - in_clause: Some(WhereClause { + in_clauses: vec![WhereClause { field: "status".to_string(), operator: WhereOperator::In, value: Value::Array(allowed_values), - }), + }], ..Default::default() }; @@ -2338,11 +2338,11 @@ mod tests { let contract = fixture.data_contract_owned(); let internal_clauses = InternalClauses { - in_clause: Some(WhereClause { + in_clauses: vec![WhereClause { field: "nonexistent".to_string(), operator: WhereOperator::In, value: Value::Array(vec![Value::I64(1)]), - }), + }], ..Default::default() }; diff --git a/packages/rs-drive/src/query/mod.rs b/packages/rs-drive/src/query/mod.rs index 72be0fbf86e..53fad48e77c 100644 --- a/packages/rs-drive/src/query/mod.rs +++ b/packages/rs-drive/src/query/mod.rs @@ -82,10 +82,10 @@ use { data_contract::{ accessors::v0::DataContractV0Getters, document_type::{accessors::DocumentTypeV0Getters, methods::DocumentTypeV0Methods}, - document_type::{DocumentTypeRef, Index, IndexProperty}, + document_type::{DocumentTypeRef, Index}, DataContract, }, - document::{document_methods::DocumentMethodsV0, Document, DocumentV0Getters}, + document::{document_methods::DocumentMethodsV0, Document}, platform_value::{btreemap_extensions::BTreeValueRemoveFromMapHelper, Value}, version::PlatformVersion, ProtocolError, @@ -132,12 +132,15 @@ pub mod conditions; mod defaults; #[cfg(any(feature = "server", feature = "verify"))] pub mod having; +mod non_primary_key_path_query; #[cfg(any(feature = "server", feature = "verify"))] pub mod ordering; #[cfg(any(feature = "server", feature = "verify"))] pub mod projection; #[cfg(any(feature = "server", feature = "verify"))] mod single_document_drive_query; +/// Versioned grouping of raw where clauses into equality / range / in buckets +pub(crate) mod where_clause_grouping; // Module declarations exclusively for "server" feature #[cfg(feature = "server")] @@ -291,8 +294,13 @@ pub struct InternalClauses { pub primary_key_in_clause: Option, /// Primary key equal clause pub primary_key_equal_clause: Option, - /// In clause - pub in_clause: Option, + /// In clauses, on distinct non-primary-key fields. + /// + /// The grammar groups any number of them structurally; whether more + /// than one is accepted is a protocol-versioned decision made at + /// path-query lowering (protocol version 14 is the first to accept + /// multiple in clauses, on consecutive index properties). + pub in_clauses: Vec, /// Range clause pub range_clause: Option, /// Equal clause @@ -310,7 +318,7 @@ impl InternalClauses { .bitxor(self.primary_key_equal_clause.is_some()) { // One is set, all rest must be empty - !(self.in_clause.is_some() + !(!self.in_clauses.is_empty() || self.range_clause.is_some() || !self.equal_clauses.is_empty()) } else { @@ -327,7 +335,7 @@ impl InternalClauses { #[cfg(any(feature = "server", feature = "verify"))] /// Returns true if self is empty. pub fn is_empty(&self) -> bool { - self.in_clause.is_none() + self.in_clauses.is_empty() && self.range_clause.is_none() && self.equal_clauses.is_empty() && self.primary_key_in_clause.is_none() @@ -336,7 +344,10 @@ impl InternalClauses { #[cfg(any(feature = "server", feature = "verify"))] /// Extracts the `WhereClause`s and returns them as type `InternalClauses`. - pub fn extract_from_clauses(all_where_clauses: Vec) -> Result { + pub fn extract_from_clauses( + all_where_clauses: Vec, + platform_version: &PlatformVersion, + ) -> Result { let primary_key_equal_clauses_array = all_where_clauses .iter() .filter_map(|where_clause| match where_clause.operator { @@ -359,8 +370,8 @@ impl InternalClauses { }) .collect::>(); - let (equal_clauses, range_clause, in_clause) = - WhereClause::group_clauses(&all_where_clauses)?; + let (equal_clauses, range_clause, in_clauses) = + WhereClause::group_clauses(&all_where_clauses, platform_version)?; let primary_key_equal_clause = match primary_key_equal_clauses_array.len() { 0 => Ok(None), @@ -395,7 +406,7 @@ impl InternalClauses { let internal_clauses = InternalClauses { primary_key_equal_clause, primary_key_in_clause, - in_clause, + in_clauses, range_clause, equal_clauses, }; @@ -423,8 +434,8 @@ impl InternalClauses { ); } - // Validate in_clause against schema - if let Some(in_clause) = &self.in_clause { + // Validate in_clauses against schema + for in_clause in &self.in_clauses { // Forbid $id in non-primary-key clauses if in_clause.field == "$id" { return QuerySyntaxSimpleValidationResult::new_with_error( @@ -521,9 +532,7 @@ impl From for Vec { fn from(clauses: InternalClauses) -> Self { let mut result: Self = clauses.equal_clauses.into_values().collect(); - if let Some(clause) = clauses.in_clause { - result.push(clause); - }; + result.extend(clauses.in_clauses); if let Some(clause) = clauses.primary_key_equal_clause { result.push(clause); }; @@ -580,7 +589,7 @@ impl<'a> DriveDocumentQuery<'a> { operator: WhereOperator::Equal, value: Value::Identifier(id.to_buffer()), }), - in_clause: None, + in_clauses: Vec::new(), range_clause: None, equal_clauses: Default::default(), }, @@ -653,13 +662,20 @@ impl<'a> DriveDocumentQuery<'a> { contract: &'a DataContract, document_type: DocumentTypeRef<'a>, config: &DriveConfig, + platform_version: &PlatformVersion, ) -> Result { let query_document_value: Value = ciborium::de::from_reader(query_cbor).map_err(|_| { Error::Query(QuerySyntaxError::DeserializationError( "unable to decode query from cbor".to_string(), )) })?; - Self::from_value(query_document_value, contract, document_type, config) + Self::from_value( + query_document_value, + contract, + document_type, + config, + platform_version, + ) } #[cfg(any(feature = "server", feature = "verify"))] @@ -669,9 +685,16 @@ impl<'a> DriveDocumentQuery<'a> { contract: &'a DataContract, document_type: DocumentTypeRef<'a>, config: &DriveConfig, + platform_version: &PlatformVersion, ) -> Result { let query_document: BTreeMap = query_value.into_btree_string_map()?; - Self::from_btree_map_value(query_document, contract, document_type, config) + Self::from_btree_map_value( + query_document, + contract, + document_type, + config, + platform_version, + ) } #[cfg(any(feature = "server", feature = "verify"))] @@ -681,6 +704,7 @@ impl<'a> DriveDocumentQuery<'a> { contract: &'a DataContract, document_type: DocumentTypeRef<'a>, config: &DriveConfig, + platform_version: &PlatformVersion, ) -> Result { if let Some(contract_id) = query_document .remove_optional_identifier("contract_id") @@ -759,7 +783,8 @@ impl<'a> DriveDocumentQuery<'a> { } })?; - let internal_clauses = InternalClauses::extract_from_clauses(all_where_clauses)?; + let internal_clauses = + InternalClauses::extract_from_clauses(all_where_clauses, platform_version)?; let start_at_option = query_document.remove("startAt"); let start_after_option = query_document.remove("startAfter"); @@ -852,6 +877,7 @@ impl<'a> DriveDocumentQuery<'a> { contract: &'a DataContract, document_type: DocumentTypeRef<'a>, config: &DriveConfig, + platform_version: &PlatformVersion, ) -> Result { let all_where_clauses: Vec = match where_clause { Value::Null => Ok(vec![]), @@ -913,6 +939,7 @@ impl<'a> DriveDocumentQuery<'a> { contract, document_type, config, + platform_version, ) } @@ -947,6 +974,7 @@ impl<'a> DriveDocumentQuery<'a> { contract: &'a DataContract, document_type: DocumentTypeRef<'a>, config: &DriveConfig, + platform_version: &PlatformVersion, ) -> Result { let limit = maybe_limit .map_or(Some(config.default_query_limit), |limit_value| { @@ -961,7 +989,8 @@ impl<'a> DriveDocumentQuery<'a> { config.max_query_limit ))))?; - let internal_clauses = InternalClauses::extract_from_clauses(where_clauses)?; + let internal_clauses = + InternalClauses::extract_from_clauses(where_clauses, platform_version)?; let order_by: IndexMap = order_by_clauses .into_iter() @@ -987,6 +1016,7 @@ impl<'a> DriveDocumentQuery<'a> { sql_string: &str, contract: &'a DataContract, config: Option<&DriveConfig>, + platform_version: &PlatformVersion, ) -> Result { let dialect: MySqlDialect = MySqlDialect {}; let statements: Vec = Parser::parse_sql(&dialect, sql_string) @@ -1107,7 +1137,8 @@ impl<'a> DriveDocumentQuery<'a> { )?; } - let internal_clauses = InternalClauses::extract_from_clauses(all_where_clauses)?; + let internal_clauses = + InternalClauses::extract_from_clauses(all_where_clauses, platform_version)?; let start_at_option = None; //todo let start_after_option = None; //todo @@ -1187,6 +1218,52 @@ impl<'a> DriveDocumentQuery<'a> { } } + #[cfg(any(feature = "server", feature = "verify"))] + /// Versioned preflight over the non-primary-key `In` clause shape. + /// + /// Runs before any cursor storage lookup or proof processing so the + /// rejection precedence matches each protocol version's contract: v0 + /// rejects more than one `In` clause with `MultipleInClauses` before a + /// `startAt`/`startAfter` document is ever fetched (matching the + /// pre-protocol-version-14 parse-time rejection), and v1 rejects the + /// unsupported multi-`In` + cursor combination with `Unsupported` + /// before spending state or proof work on the cursor. The lowering + /// keeps equivalent guards for callers that reach it directly. + pub fn validate_in_clause_shape( + &self, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive + .methods + .document + .query + .non_primary_key_path_query + { + 0 => { + if self.internal_clauses.in_clauses.len() > 1 { + return Err(Error::Query(QuerySyntaxError::MultipleInClauses( + "There should only be one in clause", + ))); + } + Ok(()) + } + 1 => { + if self.internal_clauses.in_clauses.len() > 1 && self.start_at.is_some() { + return Err(Error::Query(QuerySyntaxError::Unsupported( + "startAt/startAfter is not supported with multiple in clauses".to_string(), + ))); + } + Ok(()) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "DriveDocumentQuery::validate_in_clause_shape".to_string(), + known_versions: vec![0, 1], + received: version, + })), + } + } + #[cfg(feature = "server")] /// Operations to construct a path query. pub fn construct_path_query_operations( @@ -1197,6 +1274,7 @@ impl<'a> DriveDocumentQuery<'a> { drive_operations: &mut Vec, platform_version: &PlatformVersion, ) -> Result { + self.validate_in_clause_shape(platform_version)?; let drive_version = &platform_version.drive; // First we should get the overall document_type_path let document_type_path = self @@ -1301,6 +1379,7 @@ impl<'a> DriveDocumentQuery<'a> { starts_at_document: Option, platform_version: &PlatformVersion, ) -> Result { + self.validate_in_clause_shape(platform_version)?; // First we should get the overall document_type_path let document_type_path = self .contract @@ -1495,7 +1574,15 @@ impl<'a> DriveDocumentQuery<'a> { #[cfg(any(feature = "server", feature = "verify"))] /// Finds the best index for the query. + /// + /// Queries with more than one `In` clause use their own selection + /// ([`Self::find_best_index_for_multiple_in_clauses`]); they only + /// reach it through the v1 (protocol version 14+) path-query + /// lowering, since the v0 lowering rejects them first. pub fn find_best_index(&self, platform_version: &PlatformVersion) -> Result<&Index, Error> { + if self.internal_clauses.in_clauses.len() > 1 { + return Ok(self.find_best_index_for_multiple_in_clauses()?.0); + } let equal_fields = self .internal_clauses .equal_clauses @@ -1504,8 +1591,8 @@ impl<'a> DriveDocumentQuery<'a> { .collect::>(); let in_field = self .internal_clauses - .in_clause - .as_ref() + .in_clauses + .first() .map(|in_clause| in_clause.field.as_str()); let range_field = self .internal_clauses @@ -1565,740 +1652,42 @@ impl<'a> DriveDocumentQuery<'a> { } } - #[cfg(any(feature = "server", feature = "verify"))] - /// Returns a `Query` that either starts at or after the given document ID if given. - fn inner_query_from_starts_at_for_id( - starts_at_document: Option<&StartAtDocument>, - left_to_right: bool, - ) -> Query { - // We only need items after the start at document - let mut inner_query = Query::new_with_direction(left_to_right); - - if let Some(StartAtDocument { - document, included, .. - }) = starts_at_document - { - let start_at_key = document.id().to_vec(); - if *included { - inner_query.insert_range_from(start_at_key..) - } else { - inner_query.insert_range_after(start_at_key..) - } - } else { - // No starts at document, take all NULL items - inner_query.insert_all(); - } - inner_query - } - - #[cfg(any(feature = "server", feature = "verify"))] - /// Returns a `Query` that either starts at or after the given key. - fn inner_query_starts_from_key( - start_at_key: Option>, - left_to_right: bool, - included: bool, - ) -> Query { - // We only need items after the start at document - let mut inner_query = Query::new_with_direction(left_to_right); - - if left_to_right { - if let Some(start_at_key) = start_at_key { - if included { - inner_query.insert_range_from(start_at_key..); - } else { - inner_query.insert_range_after(start_at_key..); - } - } else { - inner_query.insert_all(); - } - } else if included { - if let Some(start_at_key) = start_at_key { - inner_query.insert_range_to_inclusive(..=start_at_key); - } else { - inner_query.insert_key(vec![]); - } - } else if let Some(start_at_key) = start_at_key { - inner_query.insert_range_to(..start_at_key); - } else { - //todo: really not sure if this is correct - // Should investigate more - inner_query.insert_key(vec![]); - } - - inner_query - } - - #[cfg(any(feature = "server", feature = "verify"))] - /// Returns a `Query` that either starts at or after the given document if given. - fn inner_query_from_starts_at( - starts_at_document: Option<&StartAtDocument>, - indexed_property: &IndexProperty, - left_to_right: bool, - platform_version: &PlatformVersion, - ) -> Result { - let mut inner_query = Query::new_with_direction(left_to_right); - if let Some(StartAtDocument { - document, - document_type, - included, - }) = starts_at_document - { - // We only need items after the start at document - let start_at_key = document.get_raw_for_document_type( - indexed_property.name.as_str(), - *document_type, - None, - platform_version, - )?; - // We want to get items starting at the start key - if let Some(start_at_key) = start_at_key { - if left_to_right { - if *included { - inner_query.insert_range_from(start_at_key..) - } else { - inner_query.insert_range_after(start_at_key..) - } - } else if *included { - inner_query.insert_range_to_inclusive(..=start_at_key) - } else { - inner_query.insert_range_to(..start_at_key) - } - } else if left_to_right { - inner_query.insert_all(); - } else { - inner_query.insert_key(vec![]); - } - } else { - // No starts at document, take all NULL items - inner_query.insert_all(); - } - Ok(inner_query) - } - - #[cfg(any(feature = "server", feature = "verify"))] - fn recursive_create_query( - left_over_index_properties: &[&IndexProperty], - unique: bool, - starts_at_document: Option<&StartAtDocument>, //for key level, included - indexed_property: &IndexProperty, - order_by: Option<&IndexMap>, - platform_version: &PlatformVersion, - ) -> Result, Error> { - match left_over_index_properties.split_first() { - None => Ok(None), - Some((first, left_over)) => { - let left_to_right = if let Some(order_by) = order_by { - order_by - .get(first.name.as_str()) - .map(|order_clause| order_clause.ascending) - .unwrap_or(first.ascending) - } else { - first.ascending - }; - - let mut inner_query = Self::inner_query_from_starts_at( - starts_at_document, - indexed_property, - left_to_right, - platform_version, - )?; - DriveDocumentQuery::recursive_insert_on_query( - &mut inner_query, - left_over, - unique, - starts_at_document, - left_to_right, - order_by, - platform_version, - )?; - Ok(Some(inner_query)) - } - } - } - - #[cfg(any(feature = "server", feature = "verify"))] - /// Recursively queries as long as there are leftover index properties. - /// The in_start_at_document_sub_path_needing_conditional is interesting. - /// It indicates whether the start at document should be applied as a conditional - /// For example if we have a tree - /// Root - /// ├── model - /// │ ├── sedan - /// │ │ ├── brand_name - /// │ │ │ ├── Honda - /// │ │ │ │ ├── car_type - /// │ │ │ │ │ ├── Accord - /// │ │ │ │ │ │ ├── 0 - /// │ │ │ │ │ │ │ ├── a47d2... - /// │ │ │ │ │ │ │ ├── e19c8... - /// │ │ │ │ │ │ │ └── f1a7b... - /// │ │ │ │ │ └── Civic - /// │ │ │ │ │ ├── 0 - /// │ │ │ │ │ │ ├── b65a7... - /// │ │ │ │ │ │ └── c43de... - /// │ │ │ ├── Toyota - /// │ │ │ │ ├── car_type - /// │ │ │ │ │ ├── Camry - /// │ │ │ │ │ │ ├── 0 - /// │ │ │ │ │ │ │ └── 1a9d2... - /// │ │ │ │ │ └── Corolla - /// │ │ │ │ │ ├── 0 - /// │ │ │ │ │ │ ├── 3f7b4... - /// │ │ │ │ │ │ ├── 4e8fa... - /// │ │ │ │ │ │ └── 9b1c6... - /// │ ├── suv - /// │ │ ├── brand_name - /// │ │ │ ├── Ford* - /// │ │ │ │ ├── car_type* - /// │ │ │ │ │ ├── Escape* - /// │ │ │ │ │ │ ├── 0 - /// │ │ │ │ │ │ │ ├── 102bc... - /// │ │ │ │ │ │ │ ├── 29f8e... <- Set After this document - /// │ │ │ │ │ │ │ └── 6b1a3... - /// │ │ │ │ │ └── Explorer - /// │ │ │ │ │ ├── 0 - /// │ │ │ │ │ │ ├── b2a9d... - /// │ │ │ │ │ │ └── f4d5c... - /// │ │ │ ├── Nissan - /// │ │ │ │ ├── car_type - /// │ │ │ │ │ ├── Rogue - /// │ │ │ │ │ │ ├── 0 - /// │ │ │ │ │ │ │ ├── 5a9c3... - /// │ │ │ │ │ │ │ └── 7e4b9... - /// │ │ │ │ │ └── Murano - /// │ │ │ │ │ ├── 0 - /// │ │ │ │ │ │ ├── 8f6a2... - /// │ │ │ │ │ │ └── 9c7d4... - /// │ ├── truck - /// │ │ ├── brand_name - /// │ │ │ ├── Ford - /// │ │ │ │ ├── car_type - /// │ │ │ │ │ ├── F-150 - /// │ │ │ │ │ │ ├── 0 - /// │ │ │ │ │ │ │ ├── 72a3b... - /// │ │ │ │ │ │ │ └── 94c8e... - /// │ │ │ │ │ └── Ranger - /// │ │ │ │ │ ├── 0 - /// │ │ │ │ │ │ ├── 3f4b1... - /// │ │ │ │ │ │ ├── 6e7d2... - /// │ │ │ │ │ │ └── 8a1f5... - /// │ │ │ ├── Toyota - /// │ │ │ │ ├── car_type - /// │ │ │ │ │ ├── Tundra - /// │ │ │ │ │ │ ├── 0 - /// │ │ │ │ │ │ │ ├── 7c9a4... - /// │ │ │ │ │ │ │ └── a5d1e... - /// │ │ │ │ │ └── Tacoma - /// │ │ │ │ │ ├── 0 - /// │ │ │ │ │ │ ├── 1e7f4... - /// │ │ │ │ │ │ └── 6b9d3... - /// - /// let's say we are asking for suv's after 29f8e - /// here the * denotes the area needing a conditional - /// We need a conditional subquery on Ford to say only things after Ford (with Ford included) - /// We need a conditional subquery on Escape to say only things after Escape (with Escape included) - fn recursive_insert_on_query( - query: &mut Query, - left_over_index_properties: &[&IndexProperty], - unique: bool, - starts_at_document: Option<&StartAtDocument>, //for key level, included - default_left_to_right: bool, - order_by: Option<&IndexMap>, - platform_version: &PlatformVersion, - ) -> Result, Error> { - match left_over_index_properties.split_first() { - None => { - match unique { - true => { - query.set_subquery_key(vec![0]); - - // In the case things are NULL we allow to have multiple values - let inner_query = Self::inner_query_from_starts_at_for_id( - starts_at_document, - true, //for ids we always go left to right - ); - query.add_conditional_subquery( - QueryItem::Key(b"".to_vec()), - Some(vec![vec![0]]), - Some(inner_query), - ); - } - false => { - query.set_subquery_key(vec![0]); - // we just get all by document id order ascending - let full_query = - Self::inner_query_from_starts_at_for_id(None, default_left_to_right); - query.set_subquery(full_query); - - let inner_query = Self::inner_query_from_starts_at_for_id( - starts_at_document, - default_left_to_right, - ); - - query.add_conditional_subquery( - QueryItem::Key(b"".to_vec()), - Some(vec![vec![0]]), - Some(inner_query), - ); - } - } - Ok(None) - } - Some((first, left_over)) => { - let left_to_right = if let Some(order_by) = order_by { - order_by - .get(first.name.as_str()) - .map(|order_clause| order_clause.ascending) - .unwrap_or(first.ascending) - } else { - first.ascending - }; - - if let Some(start_at_document_inner) = starts_at_document { - let StartAtDocument { - document, - document_type, - included, - } = start_at_document_inner; - let start_at_key = document - .get_raw_for_document_type( - first.name.as_str(), - *document_type, - None, - platform_version, - ) - .ok() - .flatten(); - - // We should always include if we have left_over - let non_conditional_included = - !left_over.is_empty() || *included || start_at_key.is_none(); - - let mut non_conditional_query = Self::inner_query_starts_from_key( - start_at_key.clone(), - left_to_right, - non_conditional_included, - ); - - // We place None here on purpose, this has been well-thought-out - // and should not change. The reason is that the path of the start - // at document is used only on the conditional subquery and not on the - // main query - // for example in the following - // Our query will be with $ownerId == a3f9b81c4d7e6a9f5b1c3e8a2d9c4f7b - // With start after 8f2d5 - // We want to get from 2024-11-17T12:45:00Z - // withdrawal - // ├── $ownerId - // │ ├── a3f9b81c4d7e6a9f5b1c3e8a2d9c4f7b - // │ │ ├── $updatedAt - // │ │ │ ├── 2024-11-17T12:45:00Z <- conditional subquery here - // │ │ │ │ ├── status - // │ │ │ │ │ ├── 0 - // │ │ │ │ │ │ ├── 7a9f1... - // │ │ │ │ │ │ └── 4b8c3... - // │ │ │ │ │ ├── 1 - // │ │ │ │ │ │ ├── 8f2d5... <- start after - // │ │ │ │ │ │ └── 5c1e4... - // │ │ │ │ │ ├── 2 - // │ │ │ │ │ │ ├── 2e7a9... - // │ │ │ │ │ │ └── 1c8b3... - // │ │ │ ├── 2024-11-18T11:25:00Z <- we want all statuses here, so normal subquery, with None as start at document - // │ │ │ │ ├── status - // │ │ │ │ │ ├── 0 - // │ │ │ │ │ │ └── 1a4f2... - // │ │ │ │ │ ├── 2 - // │ │ │ │ │ │ ├── 3e7a9... - // │ │ │ │ │ │ └── 198b4... - // │ ├── b6d7e9c4a5f2b3d8e1a7c9f4b1e8a3f - // │ │ ├── $updatedAt - // │ │ │ ├── 2024-11-17T13:30:00Z - // │ │ │ │ ├── status - // │ │ │ │ │ ├── 0 - // │ │ │ │ │ │ ├── 6d7e2... - // │ │ │ │ │ │ └── 9c7f5... - // │ │ │ │ │ ├── 3 - // │ │ │ │ │ │ ├── 3a9b7... - // │ │ │ │ │ │ └── 8e5c4... - // │ │ │ │ │ ├── 4 - // │ │ │ │ │ │ ├── 1f7a8... - // │ │ │ │ │ │ └── 2c9b3... - // println!("going to call recursive_insert_on_query on non_conditional_query {} with left_over {:?}", non_conditional_query, left_over); - DriveDocumentQuery::recursive_insert_on_query( - &mut non_conditional_query, - left_over, - unique, - None, - left_to_right, - order_by, - platform_version, - )?; - - DriveDocumentQuery::recursive_conditional_insert_on_query( - &mut non_conditional_query, - start_at_key, - left_over, - unique, - start_at_document_inner, - left_to_right, - order_by, - platform_version, - )?; - - query.set_subquery(non_conditional_query); - } else { - let mut inner_query = Query::new_with_direction(first.ascending); - inner_query.insert_all(); - DriveDocumentQuery::recursive_insert_on_query( - &mut inner_query, - left_over, - unique, - starts_at_document, - left_to_right, - order_by, - platform_version, - )?; - query.set_subquery(inner_query); - } - query.set_subquery_key(first.name.as_bytes().to_vec()); - Ok(None) - } - } - } - - #[cfg(any(feature = "server", feature = "verify"))] - #[allow(clippy::too_many_arguments)] - fn recursive_conditional_insert_on_query( - query: &mut Query, - conditional_value: Option>, - left_over_index_properties: &[&IndexProperty], - unique: bool, - starts_at_document: &StartAtDocument, - default_left_to_right: bool, - order_by: Option<&IndexMap>, - platform_version: &PlatformVersion, - ) -> Result<(), Error> { - match left_over_index_properties.split_first() { - None => { - match unique { - true => { - // In the case things are NULL we allow to have multiple values - let inner_query = Self::inner_query_from_starts_at_for_id( - Some(starts_at_document), - true, //for ids we always go left to right - ); - query.add_conditional_subquery( - QueryItem::Key(b"".to_vec()), - Some(vec![vec![0]]), - Some(inner_query), - ); - } - false => { - let inner_query = Self::inner_query_from_starts_at_for_id( - Some(starts_at_document), - default_left_to_right, - ); - - query.add_conditional_subquery( - QueryItem::Key(conditional_value.unwrap_or_default()), - Some(vec![vec![0]]), - Some(inner_query), - ); - } - } - } - Some((first, left_over)) => { - let left_to_right = if let Some(order_by) = order_by { - order_by - .get(first.name.as_str()) - .map(|order_clause| order_clause.ascending) - .unwrap_or(first.ascending) - } else { - first.ascending - }; - - let StartAtDocument { - document, - document_type, - .. - } = starts_at_document; - - let lower_start_at_key = document - .get_raw_for_document_type( - first.name.as_str(), - *document_type, - None, - platform_version, - ) - .ok() - .flatten(); - - // We include it if we are not unique, - // or if we are unique but the value is empty - let non_conditional_included = !unique || lower_start_at_key.is_none(); - - let mut non_conditional_query = Self::inner_query_starts_from_key( - lower_start_at_key.clone(), - left_to_right, - non_conditional_included, - ); - - DriveDocumentQuery::recursive_insert_on_query( - &mut non_conditional_query, - left_over, - unique, - None, - left_to_right, - order_by, - platform_version, - )?; - - DriveDocumentQuery::recursive_conditional_insert_on_query( - &mut non_conditional_query, - lower_start_at_key, - left_over, - unique, - starts_at_document, - left_to_right, - order_by, - platform_version, - )?; - - query.add_conditional_subquery( - QueryItem::Key(conditional_value.unwrap_or_default()), - Some(vec![first.name.as_bytes().to_vec()]), - Some(non_conditional_query), - ); - } - } - Ok(()) - } - #[cfg(any(feature = "server", feature = "verify"))] /// Returns a path query for non-primary keys given a document type path and starting document. + /// + /// Versioned because the set of accepted query shapes is part of the + /// consensus query contract: v0 rejects more than one `In` clause per + /// query, v1 (protocol version 14) lowers multiple `In` clauses on + /// consecutive index properties to a multi-level key-set path query. pub fn get_non_primary_key_path_query( &self, document_type_path: Vec>, starts_at_document: Option<(Document, bool)>, platform_version: &PlatformVersion, ) -> Result { - let index = self.find_best_index(platform_version)?; - let ordered_clauses: Vec<&WhereClause> = index - .properties - .iter() - .filter_map(|field| self.internal_clauses.equal_clauses.get(field.name.as_str())) - .collect(); - let (last_clause, last_clause_is_range, subquery_clause) = match &self - .internal_clauses - .in_clause + match platform_version + .drive + .methods + .document + .query + .non_primary_key_path_query { - None => match &self.internal_clauses.range_clause { - None => (ordered_clauses.last().copied(), false, None), - Some(where_clause) => (Some(where_clause), true, None), - }, - Some(in_clause) => match &self.internal_clauses.range_clause { - None => (Some(in_clause), true, None), - Some(range_clause) => { - // Both an `in` clause and a range clause are present. - // The outer path query must operate on the field that - // appears *earlier* (closer to the index root) in the - // chosen index, and the other clause becomes the leaf - // subquery. Without this ordering, a query like - // `status > 0 AND transactionIndex in [..]` on an index - // `[status, transactionIndex]` builds a path that - // terminates at the `status` subtree while the primary - // query iterates `transactionIndex` keys, silently - // returning []. See issue #2409. - let position_of = |field: &str| -> Option { - index - .properties - .iter() - .position(|p| p.name.as_str() == field) - }; - let in_pos = position_of(in_clause.field.as_str()); - let range_pos = position_of(range_clause.field.as_str()); - match (in_pos, range_pos) { - (Some(i), Some(r)) if i > r => (Some(range_clause), true, Some(in_clause)), - _ => (Some(in_clause), true, Some(range_clause)), - } - } - }, - }; - - // We need to get the terminal indexes unused by clauses. - let left_over_index_properties = index - .properties - .iter() - .filter(|field| { - !(self - .internal_clauses - .equal_clauses - .contains_key(field.name.as_str()) - || (last_clause.is_some() && last_clause.unwrap().field == field.name) - || (subquery_clause.is_some() && subquery_clause.unwrap().field == field.name)) - }) - .collect::>(); - - let intermediate_values = index - .properties - .iter() - .filter_map(|field| { - match self.internal_clauses.equal_clauses.get(field.name.as_str()) { - None => None, - Some(where_clause) => { - if !last_clause_is_range - && last_clause.is_some() - && last_clause.unwrap().field == field.name - { - //there is no need to give an intermediate value as the last clause is an equality - None - } else { - Some(self.document_type.serialize_value_for_key( - field.name.as_str(), - &where_clause.value, - platform_version, - )) - } - } - } - }) - .collect::>, ProtocolError>>() - .map_err(Error::from)?; - - let final_query = match last_clause { - None => { - // There is no last_clause which means we are using an index most likely because of an order_by, however we have no - // clauses, in this case we should use the first value of the index. - let first_index = index.properties.first().ok_or(Error::Drive( - DriveError::CorruptedContractIndexes("index must have properties".to_string()), - ))?; // Index must have properties - Self::recursive_create_query( - left_over_index_properties.as_slice(), - index.unique, - starts_at_document - .map(|(document, included)| StartAtDocument { - document, - document_type: self.document_type, - included, - }) - .as_ref(), - first_index, - Some(&self.order_by), - platform_version, - )? - .expect("Index must have left over properties if no last clause") - } - Some(where_clause) => { - let left_to_right = if where_clause.operator.is_range() { - let order_clause: &OrderClause = self - .order_by - .get(where_clause.field.as_str()) - .ok_or(Error::Query(QuerySyntaxError::MissingOrderByForRange( - "query must have an orderBy field for each range element", - )))?; - - order_clause.ascending - } else { - true - }; - - // We should set the starts at document to be included for the query if there are - // left over index properties. - - let query_starts_at_document = if left_over_index_properties.is_empty() { - &starts_at_document - } else { - &None - }; - - let mut query = where_clause.to_path_query( - self.document_type, - query_starts_at_document, - left_to_right, - platform_version, - )?; - - match subquery_clause { - None => { - Self::recursive_insert_on_query( - &mut query, - left_over_index_properties.as_slice(), - index.unique, - starts_at_document - .map(|(document, included)| StartAtDocument { - document, - document_type: self.document_type, - included, - }) - .as_ref(), - left_to_right, - Some(&self.order_by), - platform_version, - )?; - } - Some(subquery_where_clause) => { - let order_clause: &OrderClause = self - .order_by - .get(subquery_where_clause.field.as_str()) - .ok_or(Error::Query(QuerySyntaxError::MissingOrderByForRange( - "query must have an orderBy field for each range element", - )))?; - let mut subquery = subquery_where_clause.to_path_query( - self.document_type, - &starts_at_document, - order_clause.ascending, - platform_version, - )?; - Self::recursive_insert_on_query( - &mut subquery, - left_over_index_properties.as_slice(), - index.unique, - starts_at_document - .map(|(document, included)| StartAtDocument { - document, - document_type: self.document_type, - included, - }) - .as_ref(), - left_to_right, - Some(&self.order_by), - platform_version, - )?; - let subindex = subquery_where_clause.field.as_bytes().to_vec(); - query.set_subquery_key(subindex); - query.set_subquery(subquery); - } - }; - - query - } - }; - - let (intermediate_indexes, last_indexes) = - index.properties.split_at(intermediate_values.len()); - - // Now we should construct the path - let last_index = last_indexes.first().ok_or(Error::Query( - QuerySyntaxError::QueryOnDocumentTypeWithNoIndexes( - "document query has no index with fields", + 0 => self.get_non_primary_key_path_query_v0( + document_type_path, + starts_at_document, + platform_version, ), - ))?; - - let mut path = document_type_path; - - for (intermediate_index, intermediate_value) in - intermediate_indexes.iter().zip(intermediate_values.iter()) - { - path.push(intermediate_index.name.as_bytes().to_vec()); - path.push(intermediate_value.as_slice().to_vec()); + 1 => self.get_non_primary_key_path_query_v1( + document_type_path, + starts_at_document, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "DriveDocumentQuery::get_non_primary_key_path_query".to_string(), + known_versions: vec![0, 1], + received: version, + })), } - - path.push(last_index.name.as_bytes().to_vec()); - - Ok(PathQuery::new( - path, - SizedQuery::new(final_query, self.limit, self.offset), - )) } #[cfg(feature = "server")] @@ -2753,14 +2142,25 @@ mod tests { let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None) .expect("expected to serialize to cbor"); - let query = - DriveDocumentQuery::from_cbor(where_cbor.as_slice(), &contract, document_type, &config) - .expect("deserialize cbor shouldn't fail"); + let query = DriveDocumentQuery::from_cbor( + where_cbor.as_slice(), + &contract, + document_type, + &config, + PlatformVersion::latest(), + ) + .expect("deserialize cbor shouldn't fail"); let cbor = query.to_cbor().expect("should serialize cbor"); - let deserialized = DriveDocumentQuery::from_cbor(&cbor, &contract, document_type, &config) - .expect("should deserialize cbor"); + let deserialized = DriveDocumentQuery::from_cbor( + &cbor, + &contract, + document_type, + &config, + PlatformVersion::latest(), + ) + .expect("should deserialize cbor"); assert_eq!(query, deserialized); @@ -2794,6 +2194,7 @@ mod tests { &contract, document_type, &DriveConfig::default(), + PlatformVersion::latest(), ) .expect_err("all ranges must be on same field"); } @@ -2823,6 +2224,7 @@ mod tests { &contract, document_type, &DriveConfig::default(), + PlatformVersion::latest(), ) .expect_err("fields of queries must of defined supported types (where, limit, orderBy...)"); } @@ -2853,6 +2255,7 @@ mod tests { &contract, document_type, &DriveConfig::default(), + PlatformVersion::latest(), ) .expect_err("the query should not be created"); } @@ -2883,6 +2286,7 @@ mod tests { &contract, document_type, &DriveConfig::default(), + PlatformVersion::latest(), ) .expect("the query should be created"); } @@ -2912,6 +2316,7 @@ mod tests { &contract, document_type, &DriveConfig::default(), + PlatformVersion::latest(), ) .expect("query should be fine for a 255 byte long string"); } @@ -2933,7 +2338,7 @@ mod tests { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: None, + in_clauses: Vec::new(), range_clause: Some(WhereClause { field: "records.identity".to_string(), operator: WhereOperator::LessThan, @@ -3012,6 +2417,7 @@ mod tests { &contract, document_type, &DriveConfig::default(), + PlatformVersion::latest(), ) .expect("fields of queries length must be under 256 bytes long"); query @@ -3101,6 +2507,7 @@ mod tests { &contract, document_type, &DriveConfig::default(), + PlatformVersion::latest(), ) .expect("The query itself should be valid for a null type"); query @@ -3135,6 +2542,7 @@ mod tests { &contract, document_type, &DriveConfig::default(), + PlatformVersion::latest(), ) .expect("query should be valid for empty array"); @@ -3174,6 +2582,7 @@ mod tests { &contract, document_type, &DriveConfig::default(), + PlatformVersion::latest(), ) .expect("query is valid for too many elements"); @@ -3213,6 +2622,7 @@ mod tests { &contract, document_type, &DriveConfig::default(), + PlatformVersion::latest(), ) .expect("the query should be created"); @@ -3245,6 +2655,7 @@ mod tests { &contract, document_type, &DriveConfig::default(), + PlatformVersion::latest(), ) .expect_err("starts with can not start with an empty string"); } @@ -3273,6 +2684,7 @@ mod tests { &contract, document_type, &DriveConfig::default(), + PlatformVersion::latest(), ) .expect_err("starts with can not start with an empty string"); } @@ -3301,6 +2713,7 @@ mod tests { &contract, document_type, &DriveConfig::default(), + PlatformVersion::latest(), ) .expect_err("starts with can not start with an empty string"); } @@ -3329,6 +2742,7 @@ mod tests { &contract, document_type, &DriveConfig::default(), + PlatformVersion::latest(), ) .expect_err("starts with can not start with an empty string"); } @@ -3351,7 +2765,7 @@ mod tests { internal_clauses: InternalClauses { primary_key_in_clause: None, primary_key_equal_clause: None, - in_clause: Some(WhereClause { + in_clauses: vec![WhereClause { field: "status".to_string(), operator: WhereOperator::In, value: Value::Array(vec![ @@ -3361,7 +2775,7 @@ mod tests { Value::U64(3), Value::U64(4), ]), - }), + }], range_clause: None, equal_clauses: BTreeMap::default(), }, @@ -3428,4 +2842,439 @@ mod tests { Query::new_range_full().items ); } + + /// Unit coverage for the v1 multi-`In` path-query lowering. These + /// mirror the storage-backed integration tests in + /// `tests/query_tests.rs::multi_in_tests`, but exercise the lowering + /// as the pure function it is (contract in, path query out), so the + /// selection, validation, and rejection branches are covered by the + /// lib test target. + mod multiple_in_clause_lowering { + use super::*; + use crate::error::query::QuerySyntaxError; + use crate::error::Error; + + fn family_contract() -> DataContract { + json_document_to_contract( + "tests/supporting_files/contract/family/family-contract.json", + false, + PlatformVersion::latest(), + ) + .expect("expected to load family contract") + } + + fn text_array(values: &[&str]) -> Value { + Value::Array( + values + .iter() + .map(|value| Value::Text(value.to_string())) + .collect(), + ) + } + + fn in_clause(field: &str, values: &[&str]) -> WhereClause { + WhereClause { + field: field.to_string(), + operator: WhereOperator::In, + value: text_array(values), + } + } + + fn ascending_order_by(fields: &[&str]) -> IndexMap { + fields + .iter() + .map(|field| { + ( + field.to_string(), + OrderClause { + field: field.to_string(), + ascending: true, + }, + ) + }) + .collect() + } + + fn person_query<'a>( + contract: &'a DataContract, + where_clauses: Vec, + order_by_fields: &[&str], + ) -> DriveDocumentQuery<'a> { + let internal_clauses = + InternalClauses::extract_from_clauses(where_clauses, PlatformVersion::latest()) + .expect("clauses should group structurally"); + DriveDocumentQuery { + contract, + document_type: contract + .document_type_for_name("person") + .expect("person document type should exist"), + internal_clauses, + offset: None, + limit: Some(100), + order_by: ascending_order_by(order_by_fields), + start_at: None, + start_at_included: false, + block_time_ms: None, + } + } + + #[test] + fn two_in_clauses_lower_to_nested_key_sets() { + let contract = family_contract(); + let platform_version = PlatformVersion::latest(); + let query = person_query( + &contract, + vec![ + in_clause("firstName", &["Adey", "Briney"]), + in_clause("lastName", &["Kriskov", "Randolf"]), + ], + &["firstName", "lastName"], + ); + + let path_query = query + .construct_path_query(None, platform_version) + .expect("two in clauses should lower at protocol version 14"); + + // The path descends to the first in field of the + // [firstName, lastName] index + assert_eq!( + path_query.path.last().expect("path should not be empty"), + &b"firstName".to_vec() + ); + + // Outer level: one key per firstName in value + let outer = &path_query.query.query; + assert_eq!(outer.items.len(), 2); + assert!(outer.left_to_right); + + // Second level: a key set over lastName under the subquery + // path [lastName] + assert_eq!( + outer.default_subquery_branch.subquery_path, + Some(vec![b"lastName".to_vec()]) + ); + let inner = outer + .default_subquery_branch + .subquery + .as_deref() + .expect("expected a lastName subquery"); + assert_eq!(inner.items.len(), 2); + + // Terminal level: the document id tree under [0] + assert_eq!( + inner.default_subquery_branch.subquery_path, + Some(vec![vec![0]]) + ); + } + + #[test] + #[cfg(feature = "cbor_query")] + fn two_in_clauses_survive_cbor_round_trip() { + let contract = family_contract(); + let mut query = person_query( + &contract, + vec![ + in_clause("firstName", &["Adey", "Briney"]), + in_clause("lastName", &["Kriskov", "Randolf"]), + ], + &["firstName", "lastName"], + ); + // `from_cbor` defaults start_at_included to true when no cursor + // is present; align so the round trip compares equal + query.start_at_included = true; + + let cbor = query.to_cbor().expect("should serialize cbor"); + let deserialized = DriveDocumentQuery::from_cbor( + &cbor, + &contract, + contract + .document_type_for_name("person") + .expect("person document type should exist"), + &DriveConfig::default(), + PlatformVersion::latest(), + ) + .expect("should deserialize cbor"); + + assert_eq!(query, deserialized); + assert_eq!( + deserialized + .internal_clauses + .in_clauses + .iter() + .map(|in_clause| in_clause.field.as_str()) + .collect::>(), + vec!["firstName", "lastName"], + "both in clauses must survive the round trip in order" + ); + } + + #[test] + fn descending_order_by_on_left_over_property_is_honored() { + let contract = family_contract(); + let platform_version = PlatformVersion::latest(); + // [firstName, middleName, lastName]: two in levels, lastName + // left over with an explicit descending order + let mut query = person_query( + &contract, + vec![ + in_clause("firstName", &["Adey", "Briney"]), + in_clause("middleName", &["Ivanna", "Evangeline"]), + ], + &["firstName", "middleName"], + ); + query.order_by.insert( + "lastName".to_string(), + OrderClause { + field: "lastName".to_string(), + ascending: false, + }, + ); + + let path_query = query + .construct_path_query(None, platform_version) + .expect("two in clauses with a left-over order should lower"); + + let outer = &path_query.query.query; + let middle = outer + .default_subquery_branch + .subquery + .as_deref() + .expect("expected a middleName subquery"); + assert_eq!( + middle.default_subquery_branch.subquery_path, + Some(vec![b"lastName".to_vec()]) + ); + let left_over_level = middle + .default_subquery_branch + .subquery + .as_deref() + .expect("expected a lastName subquery"); + assert!( + !left_over_level.left_to_right, + "left-over lastName level must honor the descending order by" + ); + + // Without an order by entry the level falls back to the index + // property's direction (ascending) + query.order_by.shift_remove("lastName"); + let path_query = query + .construct_path_query(None, platform_version) + .expect("two in clauses should lower"); + let left_over_level = path_query + .query + .query + .default_subquery_branch + .subquery + .as_deref() + .expect("expected a middleName subquery") + .default_subquery_branch + .subquery + .as_deref() + .expect("expected a lastName subquery"); + assert!(left_over_level.left_to_right); + } + + #[test] + fn two_in_clauses_rejected_at_protocol_version_13() { + let contract = family_contract(); + let platform_version_13 = + PlatformVersion::get(13).expect("protocol version 13 should exist"); + let query = person_query( + &contract, + vec![ + in_clause("firstName", &["Adey", "Briney"]), + in_clause("lastName", &["Kriskov", "Randolf"]), + ], + &["firstName", "lastName"], + ); + + let error = query + .construct_path_query(None, platform_version_13) + .expect_err("multiple in clauses must be rejected before protocol version 14"); + assert!( + matches!(error, Error::Query(QuerySyntaxError::MultipleInClauses(_))), + "expected MultipleInClauses, got {error:?}" + ); + + query + .construct_path_query(None, PlatformVersion::latest()) + .expect("the same query should lower at protocol version 14"); + } + + #[test] + fn equality_prefix_two_in_clauses_and_trailing_range_lowering() { + let contract = family_contract(); + let platform_version = PlatformVersion::latest(); + let mut query = person_query( + &contract, + vec![ + WhereClause { + field: "age".to_string(), + operator: WhereOperator::Equal, + value: Value::U8(30), + }, + in_clause("firstName", &["Adey", "Briney"]), + in_clause("middleName", &["Ivanna", "Evangeline"]), + WhereClause { + field: "lastName".to_string(), + operator: WhereOperator::GreaterThan, + value: Value::Text("M".to_string()), + }, + ], + &["firstName", "middleName", "lastName"], + ); + query.limit = Some(50); + + // Matches the [age, firstName, middleName, lastName] index: + // equality prefix on age, then two consecutive in levels, then + // the range level + let path_query = query + .construct_path_query(None, platform_version) + .expect("equality + in + in + range should lower"); + + let path_len = path_query.path.len(); + assert_eq!(path_query.path[path_len - 3], b"age".to_vec()); + assert_eq!( + path_query.path.last().expect("path should not be empty"), + &b"firstName".to_vec() + ); + + let outer = &path_query.query.query; + assert_eq!(outer.items.len(), 2); + assert_eq!( + outer.default_subquery_branch.subquery_path, + Some(vec![b"middleName".to_vec()]) + ); + let middle = outer + .default_subquery_branch + .subquery + .as_deref() + .expect("expected a middleName subquery"); + assert_eq!(middle.items.len(), 2); + assert_eq!( + middle.default_subquery_branch.subquery_path, + Some(vec![b"lastName".to_vec()]) + ); + let range_level = middle + .default_subquery_branch + .subquery + .as_deref() + .expect("expected a lastName subquery"); + // The trailing range is a single range item, not a key set + assert_eq!(range_level.items.len(), 1); + assert_eq!( + range_level.default_subquery_branch.subquery_path, + Some(vec![vec![0]]) + ); + } + + #[test] + fn cross_product_above_cap_is_rejected() { + let contract = family_contract(); + let first_names: Vec = (0..20).map(|i| format!("First{i:02}")).collect(); + let last_names: Vec = (0..6).map(|i| format!("Last{i}")).collect(); + let query = person_query( + &contract, + vec![ + WhereClause { + field: "firstName".to_string(), + operator: WhereOperator::In, + value: Value::Array(first_names.iter().cloned().map(Value::Text).collect()), + }, + WhereClause { + field: "lastName".to_string(), + operator: WhereOperator::In, + value: Value::Array(last_names.iter().cloned().map(Value::Text).collect()), + }, + ], + &["firstName", "lastName"], + ); + + let error = query + .construct_path_query(None, PlatformVersion::latest()) + .expect_err("a 120-branch cross product must be rejected"); + assert!( + matches!(error, Error::Query(QuerySyntaxError::InvalidInClause(_))), + "expected InvalidInClause, got {error:?}" + ); + } + + #[test] + fn non_consecutive_in_fields_are_rejected() { + let contract = family_contract(); + // [firstName, middleName, lastName] holds middleName and + // lastName at positions 1 and 2 with no equality on firstName, + // so no index conforms + let query = person_query( + &contract, + vec![ + in_clause("middleName", &["Ivanna", "Evangeline"]), + in_clause("lastName", &["Kriskov", "Randolf"]), + ], + &["middleName", "lastName"], + ); + + let error = query + .construct_path_query(None, PlatformVersion::latest()) + .expect_err("non-consecutive in clauses must be rejected"); + assert!( + matches!( + error, + Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(_)) + ), + "expected WhereClauseOnNonIndexedProperty, got {error:?}" + ); + } + + #[test] + fn cursor_pagination_is_rejected() { + let contract = family_contract(); + let mut query = person_query( + &contract, + vec![ + in_clause("firstName", &["Adey", "Briney"]), + in_clause("lastName", &["Kriskov", "Randolf"]), + ], + &["firstName", "lastName"], + ); + query.start_at = Some([5u8; 32]); + query.start_at_included = false; + + let error = query + .construct_path_query(None, PlatformVersion::latest()) + .expect_err("cursor pagination with multiple in clauses must be rejected"); + assert!( + matches!(error, Error::Query(QuerySyntaxError::Unsupported(_))), + "expected Unsupported, got {error:?}" + ); + } + + #[test] + fn missing_order_by_on_an_in_field_is_rejected() { + let contract = family_contract(); + let query = person_query( + &contract, + vec![ + in_clause("firstName", &["Adey", "Briney"]), + in_clause("lastName", &["Kriskov", "Randolf"]), + ], + &["firstName"], + ); + + let error = query + .construct_path_query(None, PlatformVersion::latest()) + .expect_err("missing order by on an in field must be rejected"); + // Index selection rejects the shape first: the order-by + // continuity rule in `Index::matches` disqualifies every + // candidate index before the per-field `MissingOrderByForRange` + // guard could fire + assert!( + matches!( + error, + Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(_)) + ), + "expected WhereClauseOnNonIndexedProperty, got {error:?}" + ); + } + } } diff --git a/packages/rs-drive/src/query/non_primary_key_path_query/mod.rs b/packages/rs-drive/src/query/non_primary_key_path_query/mod.rs new file mode 100644 index 00000000000..3987fd1714a --- /dev/null +++ b/packages/rs-drive/src/query/non_primary_key_path_query/mod.rs @@ -0,0 +1,8 @@ +//! Versioned lowering of a [`DriveDocumentQuery`](crate::query::DriveDocumentQuery) +//! over a secondary index into a grovedb path query. Dispatch lives on +//! `DriveDocumentQuery::get_non_primary_key_path_query` in the parent +//! module; the per-version implementations live here so already-live +//! behavior is isolated from later edits. + +mod v0; +mod v1; diff --git a/packages/rs-drive/src/query/non_primary_key_path_query/v0/mod.rs b/packages/rs-drive/src/query/non_primary_key_path_query/v0/mod.rs new file mode 100644 index 00000000000..5848554fb86 --- /dev/null +++ b/packages/rs-drive/src/query/non_primary_key_path_query/v0/mod.rs @@ -0,0 +1,765 @@ +//! v0 of the non-primary-key path-query lowering — the behavior every +//! protocol version up to 13 committed to: at most one non-primary-key +//! `In` clause per query, with the cursor machinery that supports +//! `startAt` / `startAfter` under a single-branch ancestry. Frozen: +//! changes to already-live behavior belong in a new version module. + +use crate::error::drive::DriveError; +use crate::error::query::QuerySyntaxError; +use crate::error::Error; +use crate::query::conditions::WhereClause; +use crate::query::ordering::OrderClause; +use crate::query::{DriveDocumentQuery, StartAtDocument}; +use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; +use dpp::data_contract::document_type::IndexProperty; +use dpp::document::document_methods::DocumentMethodsV0; +use dpp::document::{Document, DocumentV0Getters}; +use dpp::version::PlatformVersion; +use dpp::ProtocolError; +use grovedb::{PathQuery, Query, QueryItem, SizedQuery}; +use indexmap::IndexMap; + +impl<'a> DriveDocumentQuery<'a> { + #[cfg(any(feature = "server", feature = "verify"))] + /// Returns a `Query` that either starts at or after the given document ID if given. + pub(in crate::query) fn inner_query_from_starts_at_for_id( + starts_at_document: Option<&StartAtDocument>, + left_to_right: bool, + ) -> Query { + // We only need items after the start at document + let mut inner_query = Query::new_with_direction(left_to_right); + + if let Some(StartAtDocument { + document, included, .. + }) = starts_at_document + { + let start_at_key = document.id().to_vec(); + if *included { + inner_query.insert_range_from(start_at_key..) + } else { + inner_query.insert_range_after(start_at_key..) + } + } else { + // No starts at document, take all NULL items + inner_query.insert_all(); + } + inner_query + } + + #[cfg(any(feature = "server", feature = "verify"))] + /// Returns a `Query` that either starts at or after the given key. + pub(in crate::query) fn inner_query_starts_from_key( + start_at_key: Option>, + left_to_right: bool, + included: bool, + ) -> Query { + // We only need items after the start at document + let mut inner_query = Query::new_with_direction(left_to_right); + + if left_to_right { + if let Some(start_at_key) = start_at_key { + if included { + inner_query.insert_range_from(start_at_key..); + } else { + inner_query.insert_range_after(start_at_key..); + } + } else { + inner_query.insert_all(); + } + } else if included { + if let Some(start_at_key) = start_at_key { + inner_query.insert_range_to_inclusive(..=start_at_key); + } else { + inner_query.insert_key(vec![]); + } + } else if let Some(start_at_key) = start_at_key { + inner_query.insert_range_to(..start_at_key); + } else { + //todo: really not sure if this is correct + // Should investigate more + inner_query.insert_key(vec![]); + } + + inner_query + } + + #[cfg(any(feature = "server", feature = "verify"))] + /// Returns a `Query` that either starts at or after the given document if given. + pub(in crate::query) fn inner_query_from_starts_at( + starts_at_document: Option<&StartAtDocument>, + indexed_property: &IndexProperty, + left_to_right: bool, + platform_version: &PlatformVersion, + ) -> Result { + let mut inner_query = Query::new_with_direction(left_to_right); + if let Some(StartAtDocument { + document, + document_type, + included, + }) = starts_at_document + { + // We only need items after the start at document + let start_at_key = document.get_raw_for_document_type( + indexed_property.name.as_str(), + *document_type, + None, + platform_version, + )?; + // We want to get items starting at the start key + if let Some(start_at_key) = start_at_key { + if left_to_right { + if *included { + inner_query.insert_range_from(start_at_key..) + } else { + inner_query.insert_range_after(start_at_key..) + } + } else if *included { + inner_query.insert_range_to_inclusive(..=start_at_key) + } else { + inner_query.insert_range_to(..start_at_key) + } + } else if left_to_right { + inner_query.insert_all(); + } else { + inner_query.insert_key(vec![]); + } + } else { + // No starts at document, take all NULL items + inner_query.insert_all(); + } + Ok(inner_query) + } + + #[cfg(any(feature = "server", feature = "verify"))] + pub(in crate::query) fn recursive_create_query( + left_over_index_properties: &[&IndexProperty], + unique: bool, + starts_at_document: Option<&StartAtDocument>, //for key level, included + indexed_property: &IndexProperty, + order_by: Option<&IndexMap>, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match left_over_index_properties.split_first() { + None => Ok(None), + Some((first, left_over)) => { + let left_to_right = if let Some(order_by) = order_by { + order_by + .get(first.name.as_str()) + .map(|order_clause| order_clause.ascending) + .unwrap_or(first.ascending) + } else { + first.ascending + }; + + let mut inner_query = Self::inner_query_from_starts_at( + starts_at_document, + indexed_property, + left_to_right, + platform_version, + )?; + DriveDocumentQuery::recursive_insert_on_query( + &mut inner_query, + left_over, + unique, + starts_at_document, + left_to_right, + order_by, + platform_version, + )?; + Ok(Some(inner_query)) + } + } + } + + #[cfg(any(feature = "server", feature = "verify"))] + /// Recursively queries as long as there are leftover index properties. + /// The in_start_at_document_sub_path_needing_conditional is interesting. + /// It indicates whether the start at document should be applied as a conditional + /// For example if we have a tree + /// Root + /// ├── model + /// │ ├── sedan + /// │ │ ├── brand_name + /// │ │ │ ├── Honda + /// │ │ │ │ ├── car_type + /// │ │ │ │ │ ├── Accord + /// │ │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ │ ├── a47d2... + /// │ │ │ │ │ │ │ ├── e19c8... + /// │ │ │ │ │ │ │ └── f1a7b... + /// │ │ │ │ │ └── Civic + /// │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ ├── b65a7... + /// │ │ │ │ │ │ └── c43de... + /// │ │ │ ├── Toyota + /// │ │ │ │ ├── car_type + /// │ │ │ │ │ ├── Camry + /// │ │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ │ └── 1a9d2... + /// │ │ │ │ │ └── Corolla + /// │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ ├── 3f7b4... + /// │ │ │ │ │ │ ├── 4e8fa... + /// │ │ │ │ │ │ └── 9b1c6... + /// │ ├── suv + /// │ │ ├── brand_name + /// │ │ │ ├── Ford* + /// │ │ │ │ ├── car_type* + /// │ │ │ │ │ ├── Escape* + /// │ │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ │ ├── 102bc... + /// │ │ │ │ │ │ │ ├── 29f8e... <- Set After this document + /// │ │ │ │ │ │ │ └── 6b1a3... + /// │ │ │ │ │ └── Explorer + /// │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ ├── b2a9d... + /// │ │ │ │ │ │ └── f4d5c... + /// │ │ │ ├── Nissan + /// │ │ │ │ ├── car_type + /// │ │ │ │ │ ├── Rogue + /// │ │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ │ ├── 5a9c3... + /// │ │ │ │ │ │ │ └── 7e4b9... + /// │ │ │ │ │ └── Murano + /// │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ ├── 8f6a2... + /// │ │ │ │ │ │ └── 9c7d4... + /// │ ├── truck + /// │ │ ├── brand_name + /// │ │ │ ├── Ford + /// │ │ │ │ ├── car_type + /// │ │ │ │ │ ├── F-150 + /// │ │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ │ ├── 72a3b... + /// │ │ │ │ │ │ │ └── 94c8e... + /// │ │ │ │ │ └── Ranger + /// │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ ├── 3f4b1... + /// │ │ │ │ │ │ ├── 6e7d2... + /// │ │ │ │ │ │ └── 8a1f5... + /// │ │ │ ├── Toyota + /// │ │ │ │ ├── car_type + /// │ │ │ │ │ ├── Tundra + /// │ │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ │ ├── 7c9a4... + /// │ │ │ │ │ │ │ └── a5d1e... + /// │ │ │ │ │ └── Tacoma + /// │ │ │ │ │ ├── 0 + /// │ │ │ │ │ │ ├── 1e7f4... + /// │ │ │ │ │ │ └── 6b9d3... + /// + /// let's say we are asking for suv's after 29f8e + /// here the * denotes the area needing a conditional + /// We need a conditional subquery on Ford to say only things after Ford (with Ford included) + /// We need a conditional subquery on Escape to say only things after Escape (with Escape included) + pub(in crate::query) fn recursive_insert_on_query( + query: &mut Query, + left_over_index_properties: &[&IndexProperty], + unique: bool, + starts_at_document: Option<&StartAtDocument>, //for key level, included + default_left_to_right: bool, + order_by: Option<&IndexMap>, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match left_over_index_properties.split_first() { + None => { + match unique { + true => { + query.set_subquery_key(vec![0]); + + // In the case things are NULL we allow to have multiple values + let inner_query = Self::inner_query_from_starts_at_for_id( + starts_at_document, + true, //for ids we always go left to right + ); + query.add_conditional_subquery( + QueryItem::Key(b"".to_vec()), + Some(vec![vec![0]]), + Some(inner_query), + ); + } + false => { + query.set_subquery_key(vec![0]); + // we just get all by document id order ascending + let full_query = + Self::inner_query_from_starts_at_for_id(None, default_left_to_right); + query.set_subquery(full_query); + + let inner_query = Self::inner_query_from_starts_at_for_id( + starts_at_document, + default_left_to_right, + ); + + query.add_conditional_subquery( + QueryItem::Key(b"".to_vec()), + Some(vec![vec![0]]), + Some(inner_query), + ); + } + } + Ok(None) + } + Some((first, left_over)) => { + let left_to_right = if let Some(order_by) = order_by { + order_by + .get(first.name.as_str()) + .map(|order_clause| order_clause.ascending) + .unwrap_or(first.ascending) + } else { + first.ascending + }; + + if let Some(start_at_document_inner) = starts_at_document { + let StartAtDocument { + document, + document_type, + included, + } = start_at_document_inner; + let start_at_key = document + .get_raw_for_document_type( + first.name.as_str(), + *document_type, + None, + platform_version, + ) + .ok() + .flatten(); + + // We should always include if we have left_over + let non_conditional_included = + !left_over.is_empty() || *included || start_at_key.is_none(); + + let mut non_conditional_query = Self::inner_query_starts_from_key( + start_at_key.clone(), + left_to_right, + non_conditional_included, + ); + + // We place None here on purpose, this has been well-thought-out + // and should not change. The reason is that the path of the start + // at document is used only on the conditional subquery and not on the + // main query + // for example in the following + // Our query will be with $ownerId == a3f9b81c4d7e6a9f5b1c3e8a2d9c4f7b + // With start after 8f2d5 + // We want to get from 2024-11-17T12:45:00Z + // withdrawal + // ├── $ownerId + // │ ├── a3f9b81c4d7e6a9f5b1c3e8a2d9c4f7b + // │ │ ├── $updatedAt + // │ │ │ ├── 2024-11-17T12:45:00Z <- conditional subquery here + // │ │ │ │ ├── status + // │ │ │ │ │ ├── 0 + // │ │ │ │ │ │ ├── 7a9f1... + // │ │ │ │ │ │ └── 4b8c3... + // │ │ │ │ │ ├── 1 + // │ │ │ │ │ │ ├── 8f2d5... <- start after + // │ │ │ │ │ │ └── 5c1e4... + // │ │ │ │ │ ├── 2 + // │ │ │ │ │ │ ├── 2e7a9... + // │ │ │ │ │ │ └── 1c8b3... + // │ │ │ ├── 2024-11-18T11:25:00Z <- we want all statuses here, so normal subquery, with None as start at document + // │ │ │ │ ├── status + // │ │ │ │ │ ├── 0 + // │ │ │ │ │ │ └── 1a4f2... + // │ │ │ │ │ ├── 2 + // │ │ │ │ │ │ ├── 3e7a9... + // │ │ │ │ │ │ └── 198b4... + // │ ├── b6d7e9c4a5f2b3d8e1a7c9f4b1e8a3f + // │ │ ├── $updatedAt + // │ │ │ ├── 2024-11-17T13:30:00Z + // │ │ │ │ ├── status + // │ │ │ │ │ ├── 0 + // │ │ │ │ │ │ ├── 6d7e2... + // │ │ │ │ │ │ └── 9c7f5... + // │ │ │ │ │ ├── 3 + // │ │ │ │ │ │ ├── 3a9b7... + // │ │ │ │ │ │ └── 8e5c4... + // │ │ │ │ │ ├── 4 + // │ │ │ │ │ │ ├── 1f7a8... + // │ │ │ │ │ │ └── 2c9b3... + // println!("going to call recursive_insert_on_query on non_conditional_query {} with left_over {:?}", non_conditional_query, left_over); + DriveDocumentQuery::recursive_insert_on_query( + &mut non_conditional_query, + left_over, + unique, + None, + left_to_right, + order_by, + platform_version, + )?; + + DriveDocumentQuery::recursive_conditional_insert_on_query( + &mut non_conditional_query, + start_at_key, + left_over, + unique, + start_at_document_inner, + left_to_right, + order_by, + platform_version, + )?; + + query.set_subquery(non_conditional_query); + } else { + let mut inner_query = Query::new_with_direction(first.ascending); + inner_query.insert_all(); + DriveDocumentQuery::recursive_insert_on_query( + &mut inner_query, + left_over, + unique, + starts_at_document, + left_to_right, + order_by, + platform_version, + )?; + query.set_subquery(inner_query); + } + query.set_subquery_key(first.name.as_bytes().to_vec()); + Ok(None) + } + } + } + + #[cfg(any(feature = "server", feature = "verify"))] + #[allow(clippy::too_many_arguments)] + pub(in crate::query) fn recursive_conditional_insert_on_query( + query: &mut Query, + conditional_value: Option>, + left_over_index_properties: &[&IndexProperty], + unique: bool, + starts_at_document: &StartAtDocument, + default_left_to_right: bool, + order_by: Option<&IndexMap>, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match left_over_index_properties.split_first() { + None => { + match unique { + true => { + // In the case things are NULL we allow to have multiple values + let inner_query = Self::inner_query_from_starts_at_for_id( + Some(starts_at_document), + true, //for ids we always go left to right + ); + query.add_conditional_subquery( + QueryItem::Key(b"".to_vec()), + Some(vec![vec![0]]), + Some(inner_query), + ); + } + false => { + let inner_query = Self::inner_query_from_starts_at_for_id( + Some(starts_at_document), + default_left_to_right, + ); + + query.add_conditional_subquery( + QueryItem::Key(conditional_value.unwrap_or_default()), + Some(vec![vec![0]]), + Some(inner_query), + ); + } + } + } + Some((first, left_over)) => { + let left_to_right = if let Some(order_by) = order_by { + order_by + .get(first.name.as_str()) + .map(|order_clause| order_clause.ascending) + .unwrap_or(first.ascending) + } else { + first.ascending + }; + + let StartAtDocument { + document, + document_type, + .. + } = starts_at_document; + + let lower_start_at_key = document + .get_raw_for_document_type( + first.name.as_str(), + *document_type, + None, + platform_version, + ) + .ok() + .flatten(); + + // We include it if we are not unique, + // or if we are unique but the value is empty + let non_conditional_included = !unique || lower_start_at_key.is_none(); + + let mut non_conditional_query = Self::inner_query_starts_from_key( + lower_start_at_key.clone(), + left_to_right, + non_conditional_included, + ); + + DriveDocumentQuery::recursive_insert_on_query( + &mut non_conditional_query, + left_over, + unique, + None, + left_to_right, + order_by, + platform_version, + )?; + + DriveDocumentQuery::recursive_conditional_insert_on_query( + &mut non_conditional_query, + lower_start_at_key, + left_over, + unique, + starts_at_document, + left_to_right, + order_by, + platform_version, + )?; + + query.add_conditional_subquery( + QueryItem::Key(conditional_value.unwrap_or_default()), + Some(vec![first.name.as_bytes().to_vec()]), + Some(non_conditional_query), + ); + } + } + Ok(()) + } + + #[cfg(any(feature = "server", feature = "verify"))] + /// v0 of the non-primary-key path query lowering: at most one `In` + /// clause per query, the behavior every protocol version up to 13 + /// committed to. + pub(in crate::query) fn get_non_primary_key_path_query_v0( + &self, + document_type_path: Vec>, + starts_at_document: Option<(Document, bool)>, + platform_version: &PlatformVersion, + ) -> Result { + if self.internal_clauses.in_clauses.len() > 1 { + return Err(Error::Query(QuerySyntaxError::MultipleInClauses( + "There should only be one in clause", + ))); + } + let index = self.find_best_index(platform_version)?; + let ordered_clauses: Vec<&WhereClause> = index + .properties + .iter() + .filter_map(|field| self.internal_clauses.equal_clauses.get(field.name.as_str())) + .collect(); + let (last_clause, last_clause_is_range, subquery_clause) = + match self.internal_clauses.in_clauses.first() { + None => match &self.internal_clauses.range_clause { + None => (ordered_clauses.last().copied(), false, None), + Some(where_clause) => (Some(where_clause), true, None), + }, + Some(in_clause) => match &self.internal_clauses.range_clause { + None => (Some(in_clause), true, None), + Some(range_clause) => { + // Both an `in` clause and a range clause are present. + // The outer path query must operate on the field that + // appears *earlier* (closer to the index root) in the + // chosen index, and the other clause becomes the leaf + // subquery. Without this ordering, a query like + // `status > 0 AND transactionIndex in [..]` on an index + // `[status, transactionIndex]` builds a path that + // terminates at the `status` subtree while the primary + // query iterates `transactionIndex` keys, silently + // returning []. See issue #2409. + let position_of = |field: &str| -> Option { + index + .properties + .iter() + .position(|p| p.name.as_str() == field) + }; + let in_pos = position_of(in_clause.field.as_str()); + let range_pos = position_of(range_clause.field.as_str()); + match (in_pos, range_pos) { + (Some(i), Some(r)) if i > r => { + (Some(range_clause), true, Some(in_clause)) + } + _ => (Some(in_clause), true, Some(range_clause)), + } + } + }, + }; + + // We need to get the terminal indexes unused by clauses. + let left_over_index_properties = index + .properties + .iter() + .filter(|field| { + !(self + .internal_clauses + .equal_clauses + .contains_key(field.name.as_str()) + || (last_clause.is_some() && last_clause.unwrap().field == field.name) + || (subquery_clause.is_some() && subquery_clause.unwrap().field == field.name)) + }) + .collect::>(); + + let intermediate_values = index + .properties + .iter() + .filter_map(|field| { + match self.internal_clauses.equal_clauses.get(field.name.as_str()) { + None => None, + Some(where_clause) => { + if !last_clause_is_range + && last_clause.is_some() + && last_clause.unwrap().field == field.name + { + //there is no need to give an intermediate value as the last clause is an equality + None + } else { + Some(self.document_type.serialize_value_for_key( + field.name.as_str(), + &where_clause.value, + platform_version, + )) + } + } + } + }) + .collect::>, ProtocolError>>() + .map_err(Error::from)?; + + let final_query = match last_clause { + None => { + // There is no last_clause which means we are using an index most likely because of an order_by, however we have no + // clauses, in this case we should use the first value of the index. + let first_index = index.properties.first().ok_or(Error::Drive( + DriveError::CorruptedContractIndexes("index must have properties".to_string()), + ))?; // Index must have properties + Self::recursive_create_query( + left_over_index_properties.as_slice(), + index.unique, + starts_at_document + .map(|(document, included)| StartAtDocument { + document, + document_type: self.document_type, + included, + }) + .as_ref(), + first_index, + Some(&self.order_by), + platform_version, + )? + .expect("Index must have left over properties if no last clause") + } + Some(where_clause) => { + let left_to_right = if where_clause.operator.is_range() { + let order_clause: &OrderClause = self + .order_by + .get(where_clause.field.as_str()) + .ok_or(Error::Query(QuerySyntaxError::MissingOrderByForRange( + "query must have an orderBy field for each range element", + )))?; + + order_clause.ascending + } else { + true + }; + + // We should set the starts at document to be included for the query if there are + // left over index properties. + + let query_starts_at_document = if left_over_index_properties.is_empty() { + &starts_at_document + } else { + &None + }; + + let mut query = where_clause.to_path_query( + self.document_type, + query_starts_at_document, + left_to_right, + platform_version, + )?; + + match subquery_clause { + None => { + Self::recursive_insert_on_query( + &mut query, + left_over_index_properties.as_slice(), + index.unique, + starts_at_document + .map(|(document, included)| StartAtDocument { + document, + document_type: self.document_type, + included, + }) + .as_ref(), + left_to_right, + Some(&self.order_by), + platform_version, + )?; + } + Some(subquery_where_clause) => { + let order_clause: &OrderClause = self + .order_by + .get(subquery_where_clause.field.as_str()) + .ok_or(Error::Query(QuerySyntaxError::MissingOrderByForRange( + "query must have an orderBy field for each range element", + )))?; + let mut subquery = subquery_where_clause.to_path_query( + self.document_type, + &starts_at_document, + order_clause.ascending, + platform_version, + )?; + Self::recursive_insert_on_query( + &mut subquery, + left_over_index_properties.as_slice(), + index.unique, + starts_at_document + .map(|(document, included)| StartAtDocument { + document, + document_type: self.document_type, + included, + }) + .as_ref(), + left_to_right, + Some(&self.order_by), + platform_version, + )?; + let subindex = subquery_where_clause.field.as_bytes().to_vec(); + query.set_subquery_key(subindex); + query.set_subquery(subquery); + } + }; + + query + } + }; + + let (intermediate_indexes, last_indexes) = + index.properties.split_at(intermediate_values.len()); + + // Now we should construct the path + let last_index = last_indexes.first().ok_or(Error::Query( + QuerySyntaxError::QueryOnDocumentTypeWithNoIndexes( + "document query has no index with fields", + ), + ))?; + + let mut path = document_type_path; + + for (intermediate_index, intermediate_value) in + intermediate_indexes.iter().zip(intermediate_values.iter()) + { + path.push(intermediate_index.name.as_bytes().to_vec()); + path.push(intermediate_value.as_slice().to_vec()); + } + + path.push(last_index.name.as_bytes().to_vec()); + + Ok(PathQuery::new( + path, + SizedQuery::new(final_query, self.limit, self.offset), + )) + } +} diff --git a/packages/rs-drive/src/query/non_primary_key_path_query/v1/mod.rs b/packages/rs-drive/src/query/non_primary_key_path_query/v1/mod.rs new file mode 100644 index 00000000000..4557cf0bdc0 --- /dev/null +++ b/packages/rs-drive/src/query/non_primary_key_path_query/v1/mod.rs @@ -0,0 +1,395 @@ +//! v1 of the non-primary-key path-query lowering (protocol version 14): +//! multiple `In` clauses on consecutive index properties lower to +//! multi-level key-set path queries. Single-`In` shapes route through +//! the v0 lowering unchanged. Conservative v1 restrictions (cursor +//! rejection, the cross-product cap, index conformity) live here, as +//! does the order-by-aware left-over recursion the multi-`In` path +//! uses in place of the v0 helper. + +use crate::error::query::QuerySyntaxError; +use crate::error::Error; +use crate::query::conditions::WhereClause; +use crate::query::ordering::OrderClause; +use crate::query::{defaults, DriveDocumentQuery}; +use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::methods::DocumentTypeV0Methods; +use dpp::data_contract::document_type::{Index, IndexProperty}; +use dpp::document::Document; +use dpp::version::PlatformVersion; +use dpp::ProtocolError; +use grovedb::{PathQuery, Query, QueryItem, SizedQuery}; +use indexmap::IndexMap; + +impl<'a> DriveDocumentQuery<'a> { + #[cfg(any(feature = "server", feature = "verify"))] + /// v1 counterpart of [`Self::recursive_insert_on_query`] for the + /// multi-`In` lowering: no cursor support (cursors are rejected by the + /// shape preflight), and each left-over level takes its direction from + /// `order_by` — falling back to the index property's — instead of + /// always using the index property's like the v0 helper does. + pub(in crate::query) fn recursive_insert_on_query_ordered( + query: &mut Query, + left_over_index_properties: &[&IndexProperty], + unique: bool, + default_left_to_right: bool, + order_by: &IndexMap, + ) { + match left_over_index_properties.split_first() { + None => match unique { + true => { + query.set_subquery_key(vec![0]); + + // In the case things are NULL we allow to have multiple values + let inner_query = Self::inner_query_from_starts_at_for_id( + None, true, //for ids we always go left to right + ); + query.add_conditional_subquery( + QueryItem::Key(b"".to_vec()), + Some(vec![vec![0]]), + Some(inner_query), + ); + } + false => { + query.set_subquery_key(vec![0]); + // we just get all by document id order ascending + let full_query = + Self::inner_query_from_starts_at_for_id(None, default_left_to_right); + query.set_subquery(full_query); + + let inner_query = + Self::inner_query_from_starts_at_for_id(None, default_left_to_right); + query.add_conditional_subquery( + QueryItem::Key(b"".to_vec()), + Some(vec![vec![0]]), + Some(inner_query), + ); + } + }, + Some((first, left_over)) => { + let left_to_right = order_by + .get(first.name.as_str()) + .map(|order_clause| order_clause.ascending) + .unwrap_or(first.ascending); + let mut inner_query = Query::new_with_direction(left_to_right); + inner_query.insert_all(); + Self::recursive_insert_on_query_ordered( + &mut inner_query, + left_over, + unique, + left_to_right, + order_by, + ); + query.set_subquery(inner_query); + query.set_subquery_key(first.name.as_bytes().to_vec()); + } + } + } + + #[cfg(any(feature = "server", feature = "verify"))] + /// v1 of the non-primary-key path query lowering (protocol version 14): + /// accepts multiple `In` clauses. Single-`In` shapes lower exactly as v0. + pub(in crate::query) fn get_non_primary_key_path_query_v1( + &self, + document_type_path: Vec>, + starts_at_document: Option<(Document, bool)>, + platform_version: &PlatformVersion, + ) -> Result { + if self.internal_clauses.in_clauses.len() > 1 { + self.get_non_primary_key_multiple_in_path_query( + document_type_path, + starts_at_document, + platform_version, + ) + } else { + self.get_non_primary_key_path_query_v0( + document_type_path, + starts_at_document, + platform_version, + ) + } + } + + #[cfg(any(feature = "server", feature = "verify"))] + /// Lowers a query with multiple `In` clauses into a path query whose + /// levels carry one key set per `In` clause, in index property order, + /// followed by an optional range level and the usual left-over / + /// terminal levels. Only reachable through the v1 lowering. + pub(in crate::query) fn get_non_primary_key_multiple_in_path_query( + &self, + document_type_path: Vec>, + starts_at_document: Option<(Document, bool)>, + platform_version: &PlatformVersion, + ) -> Result { + // Conservative v1: the cross-branch cursor machinery is not wired + // for key-set branching at more than one level, so reject cursors + // instead of shipping silently wrong pagination. + if starts_at_document.is_some() || self.start_at.is_some() { + return Err(Error::Query(QuerySyntaxError::Unsupported( + "startAt/startAfter is not supported with multiple in clauses".to_string(), + ))); + } + + let (index, ordered_in_clauses, equality_len) = + self.find_best_index_for_multiple_in_clauses()?; + + // Bound the branch enumeration: the product of the in list sizes is + // the number of index subtrees the query opens. + let mut cross_product: usize = 1; + for in_clause in &ordered_in_clauses { + let in_values = in_clause.in_values().into_data_with_error()??; + cross_product = cross_product.saturating_mul(in_values.len()); + } + if cross_product > defaults::MAX_IN_CROSS_PRODUCT_SIZE { + return Err(Error::Query(QuerySyntaxError::InvalidInClause(format!( + "the product of in clause list sizes must be at most {}, got {}", + defaults::MAX_IN_CROSS_PRODUCT_SIZE, + cross_product + )))); + } + + let left_over_index_properties = index + .properties + .iter() + .filter(|field| { + !(self + .internal_clauses + .equal_clauses + .contains_key(field.name.as_str()) + || ordered_in_clauses + .iter() + .any(|in_clause| in_clause.field == field.name) + || self + .internal_clauses + .range_clause + .as_ref() + .is_some_and(|range_clause| range_clause.field == field.name)) + }) + .collect::>(); + + // Every level that fans out (each in clause, and the range clause) + // needs an explicit ordering, like any range-class clause. + let direction_for = |field: &str| -> Result { + let order_clause: &OrderClause = self.order_by.get(field).ok_or(Error::Query( + QuerySyntaxError::MissingOrderByForRange( + "query must have an orderBy field for each range element", + ), + ))?; + Ok(order_clause.ascending) + }; + + // Build the query bottom-up. The deepest clause level is the range + // clause when present, otherwise the last in clause; the left-over + // index properties and the terminal document level hang under it. + let (mut child_field, mut child_query, deepest_left_to_right) = + match &self.internal_clauses.range_clause { + Some(range_clause) => { + let left_to_right = direction_for(range_clause.field.as_str())?; + let query = range_clause.to_path_query( + self.document_type, + &None, + left_to_right, + platform_version, + )?; + (range_clause.field.clone(), query, left_to_right) + } + None => { + let deepest_in_clause = + *ordered_in_clauses.last().expect("more than one in clause"); + let left_to_right = direction_for(deepest_in_clause.field.as_str())?; + let query = deepest_in_clause.to_path_query( + self.document_type, + &None, + left_to_right, + platform_version, + )?; + (deepest_in_clause.field.clone(), query, left_to_right) + } + }; + Self::recursive_insert_on_query_ordered( + &mut child_query, + left_over_index_properties.as_slice(), + index.unique, + deepest_left_to_right, + &self.order_by, + ); + + // Wrap the remaining in levels around it, deepest first. When a + // range clause is present every in clause wraps; otherwise the + // deepest in clause is already the leaf built above. + let wrapped_in_clauses = if self.internal_clauses.range_clause.is_some() { + ordered_in_clauses.as_slice() + } else { + &ordered_in_clauses[..ordered_in_clauses.len() - 1] + }; + for in_clause in wrapped_in_clauses.iter().rev() { + let left_to_right = direction_for(in_clause.field.as_str())?; + let mut query = in_clause.to_path_query( + self.document_type, + &None, + left_to_right, + platform_version, + )?; + query.set_subquery_key(child_field.as_bytes().to_vec()); + query.set_subquery(child_query); + child_field = in_clause.field.clone(); + child_query = query; + } + + let intermediate_values = index.properties[..equality_len] + .iter() + .map(|field| { + let where_clause = self + .internal_clauses + .equal_clauses + .get(field.name.as_str()) + .expect("equality prefix was validated during index selection"); + self.document_type.serialize_value_for_key( + field.name.as_str(), + &where_clause.value, + platform_version, + ) + }) + .collect::>, ProtocolError>>() + .map_err(Error::from)?; + + let mut path = document_type_path; + for (intermediate_index, intermediate_value) in index.properties[..equality_len] + .iter() + .zip(intermediate_values.iter()) + { + path.push(intermediate_index.name.as_bytes().to_vec()); + path.push(intermediate_value.as_slice().to_vec()); + } + path.push(child_field.as_bytes().to_vec()); + + Ok(PathQuery::new( + path, + SizedQuery::new(child_query, self.limit, self.offset), + )) + } + + #[cfg(any(feature = "server", feature = "verify"))] + /// Finds the best index for a query with more than one `In` clause, and + /// returns it together with the query's `In` clauses ordered by their + /// position in that index and the length of the equality prefix. + /// + /// A candidate index conforms when its properties decompose, left to + /// right, into: the equality-clause fields (exactly covering positions + /// `0..E`), then every `In` field on consecutive positions `E..E+K`, + /// then — when a range clause is present — the range field. The usual + /// tail rule from [`Index::matches`] still applies with the deepest `In` + /// field playing the role of the in field, as do the order-by continuity + /// rules and [`defaults::MAX_INDEX_DIFFERENCE`]. + pub(in crate::query) fn find_best_index_for_multiple_in_clauses( + &self, + ) -> Result<(&Index, Vec<&WhereClause>, usize), Error> { + let equal_clauses = &self.internal_clauses.equal_clauses; + let in_clauses = &self.internal_clauses.in_clauses; + let range_field = self + .internal_clauses + .range_clause + .as_ref() + .map(|range_clause| range_clause.field.as_str()); + + let mut fields = equal_clauses + .keys() + .map(|s| s.as_str()) + .collect::>(); + if let Some(range_field) = range_field { + fields.push(range_field); + } + fields.extend(in_clauses.iter().map(|in_clause| in_clause.field.as_str())); + + let order_by_keys: Vec<&str> = self + .order_by + .keys() + .map(|key: &String| { + let str = key.as_str(); + if !fields.contains(&str) { + fields.push(str); + } + str + }) + .collect(); + + let equality_len = equal_clauses.len(); + let mut best: Option<(&Index, Vec<&WhereClause>, u16)> = None; + for index in self.document_type.indexes().values() { + let mut positioned: Vec<(usize, &WhereClause)> = Vec::with_capacity(in_clauses.len()); + for in_clause in in_clauses { + match index + .properties + .iter() + .position(|property| property.name == in_clause.field) + { + Some(position) => positioned.push((position, in_clause)), + None => break, + } + } + if positioned.len() != in_clauses.len() { + continue; + } + positioned.sort_by_key(|(position, _)| *position); + + // The equality clauses must exactly cover the index prefix + if index.properties.len() < equality_len + || !index.properties[..equality_len] + .iter() + .all(|property| equal_clauses.contains_key(property.name.as_str())) + { + continue; + } + // The in clauses must sit on consecutive properties right after it + let consecutive_after_prefix = positioned + .iter() + .enumerate() + .all(|(i, (position, _))| *position == equality_len + i); + if !consecutive_after_prefix { + continue; + } + // A range clause must sit immediately after the in block + if let Some(range_field) = range_field { + match index.properties.get(equality_len + positioned.len()) { + Some(property) if property.name == range_field => {} + _ => continue, + } + } + + let deepest_in_field = positioned + .last() + .expect("more than one in clause") + .1 + .field + .as_str(); + let Some(difference) = index.matches(&fields, Some(deepest_in_field), &order_by_keys) + else { + continue; + }; + let ordered = positioned + .into_iter() + .map(|(_, in_clause)| in_clause) + .collect::>(); + if difference == 0 { + return Ok((index, ordered, equality_len)); + } + match &best { + Some((_, _, best_difference)) if *best_difference <= difference => {} + _ => best = Some((index, ordered, difference)), + } + } + + let (index, ordered, difference) = best.ok_or(Error::Query( + QuerySyntaxError::WhereClauseOnNonIndexedProperty(format!( + "query with multiple in clauses must be for valid indexes with the in clauses on \ + consecutive index properties after the equality clauses, valid indexes are: {:?}", + self.document_type.indexes() + )), + ))?; + if difference > defaults::MAX_INDEX_DIFFERENCE { + return Err(Error::Query(QuerySyntaxError::QueryTooFarFromIndex( + "query must better match an existing index", + ))); + } + Ok((index, ordered, equality_len)) + } +} diff --git a/packages/rs-drive/src/query/test_index.rs b/packages/rs-drive/src/query/test_index.rs index c3ace5a5810..1e4d39bdbff 100644 --- a/packages/rs-drive/src/query/test_index.rs +++ b/packages/rs-drive/src/query/test_index.rs @@ -117,6 +117,7 @@ mod tests { &contract, document_type.as_ref(), &DriveConfig::default(), + platform_version, ) .expect("query should be valid"); let index = query @@ -139,6 +140,7 @@ mod tests { &contract, document_type.as_ref(), &DriveConfig::default(), + platform_version, ) .expect("query should be valid"); let index = query @@ -166,6 +168,7 @@ mod tests { &contract, document_type.as_ref(), &DriveConfig::default(), + platform_version, ) .expect("query should be valid"); let error = query diff --git a/packages/rs-drive/src/query/where_clause_grouping/mod.rs b/packages/rs-drive/src/query/where_clause_grouping/mod.rs new file mode 100644 index 00000000000..51851b37f18 --- /dev/null +++ b/packages/rs-drive/src/query/where_clause_grouping/mod.rs @@ -0,0 +1,57 @@ +//! Versioned grouping of a query's raw where clauses into +//! `(equal_clauses, range_clause, in_clauses)`. +//! +//! The error surface for rejected shapes is part of the query contract, +//! so the grouping dispatches on +//! `platform_version.drive.methods.document.query.where_clause_grouping`: +//! v0 (protocol versions up to 13) rejects any query with more than one +//! non-primary-key `In` clause with `MultipleInClauses` before any other +//! same-field checks; v1 (protocol version 14) groups multiple `In` +//! clauses structurally, leaving their acceptance to the versioned +//! path-query lowering. + +mod v0; +mod v1; + +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::query::conditions::WhereClause; +use dpp::version::PlatformVersion; +use std::collections::BTreeMap; + +/// `(equal_clauses, range_clause, in_clauses)`. +pub(crate) type GroupedWhereClauses = ( + BTreeMap, + Option, + Vec, +); + +/// Group raw where clauses under the platform version's grammar. +pub(crate) fn group_where_clauses( + where_clauses: &[WhereClause], + platform_version: &PlatformVersion, +) -> Result { + match platform_version + .drive + .methods + .document + .query + .where_clause_grouping + { + 0 => { + let (equal_clauses, range_clause, in_clause) = + v0::group_where_clauses_v0(where_clauses)?; + Ok(( + equal_clauses, + range_clause, + in_clause.map_or_else(Vec::new, |in_clause| vec![in_clause]), + )) + } + 1 => v1::group_where_clauses_v1(where_clauses), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "group_where_clauses".to_string(), + known_versions: vec![0, 1], + received: version, + })), + } +} diff --git a/packages/rs-drive/src/query/where_clause_grouping/v0/mod.rs b/packages/rs-drive/src/query/where_clause_grouping/v0/mod.rs new file mode 100644 index 00000000000..9ae8ecf422d --- /dev/null +++ b/packages/rs-drive/src/query/where_clause_grouping/v0/mod.rs @@ -0,0 +1,231 @@ +//! v0 of the where-clause grouping: the grammar every protocol version +//! up to 13 committed to. Restored verbatim from the pre-multi-`In` +//! implementation — most importantly, the `MultipleInClauses` check runs +//! before any same-field duplicate / overlap checks on the `In` clauses, +//! so every query with more than one non-primary-key `In` clause reports +//! `MultipleInClauses` regardless of what else is wrong with it. + +use crate::error::query::QuerySyntaxError; +use crate::error::Error; +use crate::query::conditions::WhereClause; +use crate::query::conditions::WhereOperator::{ + Between, BetweenExcludeBounds, BetweenExcludeLeft, BetweenExcludeRight, Equal, GreaterThan, + GreaterThanOrEquals, In, LessThan, LessThanOrEquals, StartsWith, +}; +use dpp::platform_value::Value; +use std::collections::{BTreeMap, BTreeSet}; + +/// `(equal_clauses, range_clause, in_clause)` — at most one `In` clause. +pub(super) type GroupedWhereClausesV0 = ( + BTreeMap, + Option, + Option, +); + +pub(super) fn group_where_clauses_v0( + where_clauses: &[WhereClause], +) -> Result { + if where_clauses.is_empty() { + return Ok((BTreeMap::new(), None, None)); + } + let equal_clauses_array = + where_clauses + .iter() + .filter_map(|where_clause| match where_clause.operator { + Equal => match where_clause.is_identifier() { + true => None, + false => Some(where_clause.clone()), + }, + _ => None, + }); + let mut known_fields: BTreeSet = BTreeSet::new(); + let equal_clauses: BTreeMap = equal_clauses_array + .into_iter() + .map(|where_clause| { + if known_fields.contains(&where_clause.field) { + Err(Error::Query( + QuerySyntaxError::DuplicateNonGroupableClauseSameField( + "duplicate equality fields", + ), + )) + } else { + known_fields.insert(where_clause.field.clone()); + Ok((where_clause.field.clone(), where_clause)) + } + }) + .collect::, Error>>()?; + + let in_clauses_array = where_clauses + .iter() + .filter_map(|where_clause| match where_clause.operator { + In => match where_clause.is_identifier() { + true => None, + false => Some(where_clause.clone()), + }, + _ => None, + }) + .collect::>(); + + let in_clause = match in_clauses_array.len() { + 0 => Ok(None), + 1 => { + let clause = in_clauses_array.first().expect("there must be a value"); + if known_fields.contains(&clause.field) { + Err(Error::Query( + QuerySyntaxError::DuplicateNonGroupableClauseSameField( + "in clause has same field as an equality clause", + ), + )) + } else { + known_fields.insert(clause.field.clone()); + Ok(Some(clause.clone())) + } + } + _ => Err(Error::Query(QuerySyntaxError::MultipleInClauses( + "There should only be one in clause", + ))), + }?; + + // In order to group range clauses + let groupable_range_clauses: Vec<&WhereClause> = where_clauses + .iter() + .filter(|where_clause| match where_clause.operator { + Equal => false, + In => false, + GreaterThan => true, + GreaterThanOrEquals => true, + LessThan => true, + LessThanOrEquals => true, + StartsWith => false, + Between => false, + BetweenExcludeBounds => false, + BetweenExcludeRight => false, + BetweenExcludeLeft => false, + }) + .collect(); + + let non_groupable_range_clauses: Vec<&WhereClause> = where_clauses + .iter() + .filter(|where_clause| match where_clause.operator { + Equal => false, + In => false, + GreaterThan => false, + GreaterThanOrEquals => false, + LessThan => false, + LessThanOrEquals => false, + StartsWith => true, + Between => true, + BetweenExcludeBounds => true, + BetweenExcludeRight => true, + BetweenExcludeLeft => true, + }) + .collect(); + + let range_clause = if non_groupable_range_clauses.is_empty() { + if groupable_range_clauses.is_empty() { + Ok(None) + } else if groupable_range_clauses.len() == 1 { + let clause = *groupable_range_clauses.first().unwrap(); + if known_fields.contains(clause.field.as_str()) { + Err(Error::Query( + QuerySyntaxError::InvalidWhereClauseComponents( + "in clause has same field as an equality clause", + ), + )) + } else { + Ok(Some(clause.clone())) + } + } else if groupable_range_clauses.len() > 2 { + Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( + "there can only be at most 2 range clauses that must be on the same field", + ))) + } else { + let first_field = groupable_range_clauses.first().unwrap().field.as_str(); + if known_fields.contains(first_field) { + Err(Error::Query( + QuerySyntaxError::InvalidWhereClauseComponents( + "a range clause has same field as an equality or in clause", + ), + )) + } else if groupable_range_clauses + .iter() + .any(|&z| z.field.as_str() != first_field) + { + Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( + "all ranges must be on same field", + ))) + } else { + let lower_upper_error = || { + Error::Query(QuerySyntaxError::RangeClausesNotGroupable( + "lower and upper bounds must be passed if providing 2 ranges", + )) + }; + + // we need to find the bounds of the clauses + let lower_bounds_clause = + WhereClause::lower_bound_clause(groupable_range_clauses.as_slice())? + .ok_or_else(lower_upper_error)?; + let upper_bounds_clause = + WhereClause::upper_bound_clause(groupable_range_clauses.as_slice())? + .ok_or_else(lower_upper_error)?; + + let operator = match (lower_bounds_clause.operator, upper_bounds_clause.operator) { + (GreaterThanOrEquals, LessThanOrEquals) => Some(Between), + (GreaterThanOrEquals, LessThan) => Some(BetweenExcludeRight), + (GreaterThan, LessThanOrEquals) => Some(BetweenExcludeLeft), + (GreaterThan, LessThan) => Some(BetweenExcludeBounds), + _ => None, + } + .ok_or_else(lower_upper_error)?; + + if upper_bounds_clause + .less_than(lower_bounds_clause, operator == BetweenExcludeBounds)? + { + return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( + "lower bounds must be under upper bounds", + ))); + } + + Ok(Some(WhereClause { + field: groupable_range_clauses.first().unwrap().field.clone(), + operator, + value: Value::Array(vec![ + lower_bounds_clause.value.clone(), + upper_bounds_clause.value.clone(), + ]), + })) + } + } + } else if non_groupable_range_clauses.len() == 1 && groupable_range_clauses.is_empty() { + let where_clause = *non_groupable_range_clauses.first().unwrap(); + if where_clause.operator == StartsWith { + // Starts with must null be against an empty string + if let Value::Text(text) = &where_clause.value { + if text.is_empty() { + return Err(Error::Query(QuerySyntaxError::StartsWithIllegalString( + "starts with can not start with an empty string", + ))); + } + } + } + if known_fields.contains(where_clause.field.as_str()) { + Err(Error::Query( + QuerySyntaxError::DuplicateNonGroupableClauseSameField( + "a non groupable range clause has same field as an equality or in clause", + ), + )) + } else { + Ok(Some(where_clause.clone())) + } + } else if groupable_range_clauses.is_empty() { + Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( + "there can not be more than 1 non groupable range clause", + ))) + } else { + Err(Error::Query(QuerySyntaxError::RangeClausesNotGroupable( + "clauses are not groupable", + ))) + }?; + + Ok((equal_clauses, range_clause, in_clause)) +} diff --git a/packages/rs-drive/src/query/where_clause_grouping/v1/mod.rs b/packages/rs-drive/src/query/where_clause_grouping/v1/mod.rs new file mode 100644 index 00000000000..e7d6b424c63 --- /dev/null +++ b/packages/rs-drive/src/query/where_clause_grouping/v1/mod.rs @@ -0,0 +1,230 @@ +//! v1 of the where-clause grouping (protocol version 14): multiple +//! non-primary-key `In` clauses group structurally, in query order. +//! Same-field duplicates and overlaps with equality clauses are reported +//! per clause (`DuplicateNonGroupableClauseSameField`) instead of the v0 +//! blanket `MultipleInClauses`; whether more than one `In` clause is +//! *accepted* stays a decision of the versioned path-query lowering. +//! +//! Everything except the `In` handling is byte-identical to v0. + +use crate::error::query::QuerySyntaxError; +use crate::error::Error; +use crate::query::conditions::WhereClause; +use crate::query::conditions::WhereOperator::{ + Between, BetweenExcludeBounds, BetweenExcludeLeft, BetweenExcludeRight, Equal, GreaterThan, + GreaterThanOrEquals, In, LessThan, LessThanOrEquals, StartsWith, +}; +use dpp::platform_value::Value; +use std::collections::{BTreeMap, BTreeSet}; + +/// `(equal_clauses, range_clause, in_clauses)` — any number of `In` +/// clauses on distinct fields. +pub(super) type GroupedWhereClausesV1 = ( + BTreeMap, + Option, + Vec, +); + +pub(super) fn group_where_clauses_v1( + where_clauses: &[WhereClause], +) -> Result { + if where_clauses.is_empty() { + return Ok((BTreeMap::new(), None, Vec::new())); + } + let equal_clauses_array = + where_clauses + .iter() + .filter_map(|where_clause| match where_clause.operator { + Equal => match where_clause.is_identifier() { + true => None, + false => Some(where_clause.clone()), + }, + _ => None, + }); + let mut known_fields: BTreeSet = BTreeSet::new(); + let equal_clauses: BTreeMap = equal_clauses_array + .into_iter() + .map(|where_clause| { + if known_fields.contains(&where_clause.field) { + Err(Error::Query( + QuerySyntaxError::DuplicateNonGroupableClauseSameField( + "duplicate equality fields", + ), + )) + } else { + known_fields.insert(where_clause.field.clone()); + Ok((where_clause.field.clone(), where_clause)) + } + }) + .collect::, Error>>()?; + + let in_clauses_array = where_clauses + .iter() + .filter_map(|where_clause| match where_clause.operator { + In => match where_clause.is_identifier() { + true => None, + false => Some(where_clause.clone()), + }, + _ => None, + }) + .collect::>(); + + let in_clauses = in_clauses_array + .into_iter() + .map(|clause| { + if known_fields.contains(&clause.field) { + Err(Error::Query( + QuerySyntaxError::DuplicateNonGroupableClauseSameField( + "in clause has same field as an equality or in clause", + ), + )) + } else { + known_fields.insert(clause.field.clone()); + Ok(clause) + } + }) + .collect::, Error>>()?; + + // In order to group range clauses + let groupable_range_clauses: Vec<&WhereClause> = where_clauses + .iter() + .filter(|where_clause| match where_clause.operator { + Equal => false, + In => false, + GreaterThan => true, + GreaterThanOrEquals => true, + LessThan => true, + LessThanOrEquals => true, + StartsWith => false, + Between => false, + BetweenExcludeBounds => false, + BetweenExcludeRight => false, + BetweenExcludeLeft => false, + }) + .collect(); + + let non_groupable_range_clauses: Vec<&WhereClause> = where_clauses + .iter() + .filter(|where_clause| match where_clause.operator { + Equal => false, + In => false, + GreaterThan => false, + GreaterThanOrEquals => false, + LessThan => false, + LessThanOrEquals => false, + StartsWith => true, + Between => true, + BetweenExcludeBounds => true, + BetweenExcludeRight => true, + BetweenExcludeLeft => true, + }) + .collect(); + + let range_clause = if non_groupable_range_clauses.is_empty() { + if groupable_range_clauses.is_empty() { + Ok(None) + } else if groupable_range_clauses.len() == 1 { + let clause = *groupable_range_clauses.first().unwrap(); + if known_fields.contains(clause.field.as_str()) { + Err(Error::Query( + QuerySyntaxError::InvalidWhereClauseComponents( + "in clause has same field as an equality clause", + ), + )) + } else { + Ok(Some(clause.clone())) + } + } else if groupable_range_clauses.len() > 2 { + Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( + "there can only be at most 2 range clauses that must be on the same field", + ))) + } else { + let first_field = groupable_range_clauses.first().unwrap().field.as_str(); + if known_fields.contains(first_field) { + Err(Error::Query( + QuerySyntaxError::InvalidWhereClauseComponents( + "a range clause has same field as an equality or in clause", + ), + )) + } else if groupable_range_clauses + .iter() + .any(|&z| z.field.as_str() != first_field) + { + Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( + "all ranges must be on same field", + ))) + } else { + let lower_upper_error = || { + Error::Query(QuerySyntaxError::RangeClausesNotGroupable( + "lower and upper bounds must be passed if providing 2 ranges", + )) + }; + + // we need to find the bounds of the clauses + let lower_bounds_clause = + WhereClause::lower_bound_clause(groupable_range_clauses.as_slice())? + .ok_or_else(lower_upper_error)?; + let upper_bounds_clause = + WhereClause::upper_bound_clause(groupable_range_clauses.as_slice())? + .ok_or_else(lower_upper_error)?; + + let operator = match (lower_bounds_clause.operator, upper_bounds_clause.operator) { + (GreaterThanOrEquals, LessThanOrEquals) => Some(Between), + (GreaterThanOrEquals, LessThan) => Some(BetweenExcludeRight), + (GreaterThan, LessThanOrEquals) => Some(BetweenExcludeLeft), + (GreaterThan, LessThan) => Some(BetweenExcludeBounds), + _ => None, + } + .ok_or_else(lower_upper_error)?; + + if upper_bounds_clause + .less_than(lower_bounds_clause, operator == BetweenExcludeBounds)? + { + return Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( + "lower bounds must be under upper bounds", + ))); + } + + Ok(Some(WhereClause { + field: groupable_range_clauses.first().unwrap().field.clone(), + operator, + value: Value::Array(vec![ + lower_bounds_clause.value.clone(), + upper_bounds_clause.value.clone(), + ]), + })) + } + } + } else if non_groupable_range_clauses.len() == 1 && groupable_range_clauses.is_empty() { + let where_clause = *non_groupable_range_clauses.first().unwrap(); + if where_clause.operator == StartsWith { + // Starts with must null be against an empty string + if let Value::Text(text) = &where_clause.value { + if text.is_empty() { + return Err(Error::Query(QuerySyntaxError::StartsWithIllegalString( + "starts with can not start with an empty string", + ))); + } + } + } + if known_fields.contains(where_clause.field.as_str()) { + Err(Error::Query( + QuerySyntaxError::DuplicateNonGroupableClauseSameField( + "a non groupable range clause has same field as an equality or in clause", + ), + )) + } else { + Ok(Some(where_clause.clone())) + } + } else if groupable_range_clauses.is_empty() { + Err(Error::Query(QuerySyntaxError::MultipleRangeClauses( + "there can not be more than 1 non groupable range clause", + ))) + } else { + Err(Error::Query(QuerySyntaxError::RangeClausesNotGroupable( + "clauses are not groupable", + ))) + }?; + + Ok((equal_clauses, range_clause, in_clauses)) +} diff --git a/packages/rs-drive/src/verify/document/verify_start_at_document_in_proof/v0/mod.rs b/packages/rs-drive/src/verify/document/verify_start_at_document_in_proof/v0/mod.rs index f6a212a0955..301afe65148 100644 --- a/packages/rs-drive/src/verify/document/verify_start_at_document_in_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/document/verify_start_at_document_in_proof/v0/mod.rs @@ -37,6 +37,10 @@ impl DriveDocumentQuery<'_> { document_id: [u8; 32], platform_version: &PlatformVersion, ) -> Result<(RootHash, Option), Error> { + // Reject unsupported in-clause shapes before doing any proof work, + // mirroring the server-side preflight that runs before the cursor + // document is fetched. + self.validate_in_clause_shape(platform_version)?; let (start_at_document_path, start_at_document_key) = self.start_at_document_path_and_key(&document_id); let path_query = PathQuery::new_single_key( diff --git a/packages/rs-drive/tests/dashpay.rs b/packages/rs-drive/tests/dashpay.rs index 1dbc23e8aa2..fdc69914b31 100644 --- a/packages/rs-drive/tests/dashpay.rs +++ b/packages/rs-drive/tests/dashpay.rs @@ -37,6 +37,7 @@ mod contact_request { ), &data_contract, None, + platform_version, ) .expect("failed to create query"); @@ -73,6 +74,7 @@ mod contact_request { ), &data_contract, None, + platform_version, ) .expect("failed to create query"); @@ -109,6 +111,7 @@ mod contact_request { ), &data_contract, None, + platform_version, ) .expect("failed to create query"); @@ -145,6 +148,7 @@ mod contact_request { ), &data_contract, None, + platform_version, ) .expect("failed to create query"); @@ -182,6 +186,7 @@ mod contact_request { ), &data_contract, None, + platform_version, ) .expect("failed to create query"); diff --git a/packages/rs-drive/tests/masternode_rewards.rs b/packages/rs-drive/tests/masternode_rewards.rs index 0c7fc4bbc81..ef5462e5629 100644 --- a/packages/rs-drive/tests/masternode_rewards.rs +++ b/packages/rs-drive/tests/masternode_rewards.rs @@ -37,6 +37,7 @@ mod reward_share { ), &data_contract, None, + platform_version, ) .expect("failed to create query"); @@ -74,6 +75,7 @@ mod reward_share { ), &data_contract, None, + platform_version, ) .expect("failed to create query"); diff --git a/packages/rs-drive/tests/query_tests.rs b/packages/rs-drive/tests/query_tests.rs index e2aab513220..bdcd8ab5576 100644 --- a/packages/rs-drive/tests/query_tests.rs +++ b/packages/rs-drive/tests/query_tests.rs @@ -1208,6 +1208,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -1256,6 +1257,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -1323,6 +1325,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -1654,6 +1657,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -1710,6 +1714,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -1761,6 +1766,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -1813,6 +1819,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -1873,6 +1880,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -1921,6 +1929,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -1981,6 +1990,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -2040,6 +2050,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -2658,6 +2669,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -2989,6 +3001,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -3045,6 +3058,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -3096,6 +3110,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -3148,6 +3163,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -3208,6 +3224,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -3256,6 +3273,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -3316,6 +3334,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -3375,6 +3394,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -4045,6 +4065,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -4126,6 +4147,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -4185,6 +4207,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -4238,6 +4261,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -4297,6 +4321,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -4360,13 +4385,18 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("should build query"); let sql_string = "select * from person order by firstName asc limit 100"; - let query2 = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query2 = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); assert_eq!(query1, query2); @@ -4385,13 +4415,18 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("should build query"); let sql_string = "select * from person where firstName = 'Chris'"; - let query2 = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query2 = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); assert_eq!(query1, query2); @@ -4414,14 +4449,19 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("should build query"); let sql_string = "select * from person where firstName < 'Chris' order by firstName asc limit 100"; - let query2 = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query2 = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); assert_eq!(query1, query2); @@ -4444,14 +4484,19 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("should build query"); let sql_string = "select * from person where firstName like 'C%' order by firstName asc limit 100"; - let query2 = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query2 = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); assert_eq!(query1, query2); @@ -4475,13 +4520,18 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("should build query"); let sql_string = "select * from person where firstName > 'Chris' and firstName <= 'Noellyn' order by firstName asc limit 100"; - let query2 = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query2 = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); assert_eq!(query1, query2); @@ -4505,14 +4555,19 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("should build query"); let sql_string = "select * from person where firstName in ('a', 'b') order by firstName limit 100"; - let query2 = - DriveDocumentQuery::from_sql_expr(sql_string, &contract, Some(&DriveConfig::default())) - .expect("should build query"); + let query2 = DriveDocumentQuery::from_sql_expr( + sql_string, + &contract, + Some(&DriveConfig::default()), + platform_version, + ) + .expect("should build query"); assert_eq!(query1, query2); } @@ -4575,6 +4630,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -5014,6 +5070,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -5065,6 +5122,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -5146,6 +5204,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -5205,6 +5264,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -5280,6 +5340,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -5336,6 +5397,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -5384,6 +5446,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -5575,6 +5638,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -5671,6 +5735,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -5767,6 +5832,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -5863,6 +5929,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -5959,6 +6026,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -6152,6 +6220,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); @@ -6359,6 +6428,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); @@ -6579,6 +6649,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -6633,6 +6704,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -6688,6 +6760,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -6773,6 +6846,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -6872,6 +6946,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -6976,6 +7051,7 @@ mod tests { &contract, domain_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -7054,6 +7130,7 @@ mod tests { &contract, withdrawal_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -7232,6 +7309,7 @@ mod tests { &contract, withdrawal_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -7599,6 +7677,7 @@ mod tests { &contract, document_type, &DriveConfig::default(), + platform_version, ) .expect("should create a query from cbor"); @@ -7659,6 +7738,7 @@ mod tests { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); @@ -7669,3 +7749,886 @@ mod tests { assert!(!proof.is_empty(), "proof should not be empty"); } } + +#[cfg(feature = "server")] +#[cfg(test)] +mod multi_in_tests { + //! Multiple `In` clauses on consecutive compound-index properties + //! (protocol version 14+). Each positive case cross-checks the + //! index-driven results against a brute-force filter over every + //! stored document, and round-trips the proof against the live + //! root hash. + + use super::*; + + /// All people as (serialized bytes, document) pairs. + fn all_people( + drive: &Drive, + contract: &DataContract, + platform_version: &PlatformVersion, + ) -> Vec<(Vec, Document)> { + let person_document_type = contract + .document_type_for_name("person") + .expect("contract should have a person document type"); + let query_value = json!({ "limit": 100 }); + let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None) + .expect("expected to serialize to cbor"); + let query = DriveDocumentQuery::from_cbor( + where_cbor.as_slice(), + contract, + person_document_type, + &drive.config, + platform_version, + ) + .expect("query should be built"); + let (results, _, _) = query + .execute_raw_results_no_proof(drive, None, None, platform_version) + .expect("expected to fetch all people"); + results + .into_iter() + .map(|bytes| { + let document = + Document::from_bytes(bytes.as_slice(), person_document_type, platform_version) + .expect("document should deserialize"); + (bytes, document) + }) + .collect() + } + + fn text_field(document: &Document, field: &str) -> String { + document + .get(field) + .expect("field should exist") + .as_text() + .expect("field should be text") + .to_string() + } + + /// Run `query_value` both without proof and with proof, assert the + /// proof verifies against the live root hash and returns identical + /// results, and return the deserialized documents. + fn run_query_with_proof_round_trip( + drive: &Drive, + contract: &DataContract, + query_value: serde_json::Value, + platform_version: &PlatformVersion, + ) -> Vec { + let person_document_type = contract + .document_type_for_name("person") + .expect("contract should have a person document type"); + let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None) + .expect("expected to serialize to cbor"); + let query = DriveDocumentQuery::from_cbor( + where_cbor.as_slice(), + contract, + person_document_type, + &drive.config, + platform_version, + ) + .expect("query should be built"); + let (results, _, _) = query + .execute_raw_results_no_proof(drive, None, None, platform_version) + .expect("query should execute"); + + let root_hash = drive + .grove + .root_hash(None, &platform_version.drive.grove_version) + .unwrap() + .expect("there is always a root hash"); + let (proof_root_hash, proof_results, _) = query + .execute_with_proof_only_get_elements(drive, None, None, platform_version) + .expect("proof should be generated and verified"); + assert_eq!(root_hash, proof_root_hash); + assert_eq!(results, proof_results); + + results + .into_iter() + .map(|bytes| { + Document::from_bytes(bytes.as_slice(), person_document_type, platform_version) + .expect("document should deserialize") + }) + .collect() + } + + #[test] + fn test_two_in_clauses_on_compound_index() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup_family_tests(10, 73509, platform_version); + let people = all_people(&drive, &contract, platform_version); + + // Pick in lists that partially overlap the stored data + let first_names: Vec = { + let mut names: Vec = people + .iter() + .map(|(_, document)| text_field(document, "firstName")) + .collect(); + names.sort(); + names.dedup(); + names.into_iter().take(3).collect() + }; + let last_names: Vec = { + let mut names: Vec = people + .iter() + .map(|(_, document)| text_field(document, "lastName")) + .collect(); + names.sort(); + names.dedup(); + names.into_iter().take(3).collect() + }; + assert!(first_names.len() >= 2, "fixture should have enough names"); + assert!(last_names.len() >= 2, "fixture should have enough names"); + + let query_value = json!({ + "where": [ + ["firstName", "in", first_names], + ["lastName", "in", last_names], + ], + "limit": 100, + "orderBy": [ + ["firstName", "asc"], + ["lastName", "asc"], + ] + }); + let documents = + run_query_with_proof_round_trip(&drive, &contract, query_value, platform_version); + + // Brute-force expectation in index traversal order + let mut expected: Vec<(String, String)> = people + .iter() + .filter_map(|(_, document)| { + let first_name = text_field(document, "firstName"); + let last_name = text_field(document, "lastName"); + (first_names.contains(&first_name) && last_names.contains(&last_name)) + .then_some((first_name, last_name)) + }) + .collect(); + expected.sort(); + assert!(!expected.is_empty(), "fixture should produce matches"); + + let returned: Vec<(String, String)> = documents + .iter() + .map(|document| { + ( + text_field(document, "firstName"), + text_field(document, "lastName"), + ) + }) + .collect(); + assert_eq!(returned, expected); + } + + #[test] + fn test_two_in_clauses_descending_first_level() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup_family_tests(10, 73509, platform_version); + let people = all_people(&drive, &contract, platform_version); + + let first_names: Vec = { + let mut names: Vec = people + .iter() + .map(|(_, document)| text_field(document, "firstName")) + .collect(); + names.sort(); + names.dedup(); + names + }; + let last_names: Vec = { + let mut names: Vec = people + .iter() + .map(|(_, document)| text_field(document, "lastName")) + .collect(); + names.sort(); + names.dedup(); + names + }; + + let query_value = json!({ + "where": [ + ["firstName", "in", first_names], + ["lastName", "in", last_names], + ], + "limit": 100, + "orderBy": [ + ["firstName", "desc"], + ["lastName", "asc"], + ] + }); + let documents = + run_query_with_proof_round_trip(&drive, &contract, query_value, platform_version); + + let mut expected: Vec<(String, String)> = people + .iter() + .map(|(_, document)| { + ( + text_field(document, "firstName"), + text_field(document, "lastName"), + ) + }) + .collect(); + // firstName descending, lastName ascending within it + expected.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1))); + + let returned: Vec<(String, String)> = documents + .iter() + .map(|document| { + ( + text_field(document, "firstName"), + text_field(document, "lastName"), + ) + }) + .collect(); + assert_eq!(returned, expected); + } + + #[test] + fn test_three_in_clauses_on_compound_index() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup_family_tests(10, 73509, platform_version); + let people = all_people(&drive, &contract, platform_version); + + let names_of = |field: &str| -> Vec { + let mut names: Vec = people + .iter() + .map(|(_, document)| text_field(document, field)) + .collect(); + names.sort(); + names.dedup(); + names + }; + // Keep the cross product under the 100-branch cap (3 x 3 x 10 = 90) + // while guaranteeing matches: the shortened lists keep the first + // document's values. + let mut first_names = names_of("firstName"); + first_names.sort_by_key(|name| *name != text_field(&people[0].1, "firstName")); + first_names.truncate(3); + first_names.sort(); + let mut middle_names = names_of("middleName"); + middle_names.sort_by_key(|name| *name != text_field(&people[0].1, "middleName")); + middle_names.truncate(3); + middle_names.sort(); + let last_names = names_of("lastName"); + + let query_value = json!({ + "where": [ + ["firstName", "in", first_names], + ["middleName", "in", middle_names], + ["lastName", "in", last_names], + ], + "limit": 100, + "orderBy": [ + ["firstName", "asc"], + ["middleName", "asc"], + ["lastName", "asc"], + ] + }); + let documents = + run_query_with_proof_round_trip(&drive, &contract, query_value, platform_version); + + let mut expected: Vec<(String, String, String)> = people + .iter() + .filter_map(|(_, document)| { + let first_name = text_field(document, "firstName"); + let middle_name = text_field(document, "middleName"); + (first_names.contains(&first_name) && middle_names.contains(&middle_name)) + .then_some((first_name, middle_name, text_field(document, "lastName"))) + }) + .collect(); + expected.sort(); + assert!(!expected.is_empty(), "fixture should produce matches"); + + let returned: Vec<(String, String, String)> = documents + .iter() + .map(|document| { + ( + text_field(document, "firstName"), + text_field(document, "middleName"), + text_field(document, "lastName"), + ) + }) + .collect(); + assert_eq!(returned, expected); + } + + #[test] + fn test_two_in_clauses_with_trailing_range() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup_family_tests(10, 73509, platform_version); + let people = all_people(&drive, &contract, platform_version); + + let names_of = |field: &str| -> Vec { + let mut names: Vec = people + .iter() + .map(|(_, document)| text_field(document, field)) + .collect(); + names.sort(); + names.dedup(); + names + }; + let first_names = names_of("firstName"); + let middle_names = names_of("middleName"); + + let query_value = json!({ + "where": [ + ["firstName", "in", first_names], + ["middleName", "in", middle_names], + ["lastName", ">", "M"], + ], + "limit": 100, + "orderBy": [ + ["firstName", "asc"], + ["middleName", "asc"], + ["lastName", "asc"], + ] + }); + let documents = + run_query_with_proof_round_trip(&drive, &contract, query_value, platform_version); + + let mut expected: Vec<(String, String, String)> = people + .iter() + .filter_map(|(_, document)| { + let last_name = text_field(document, "lastName"); + (last_name.as_str() > "M").then(|| { + ( + text_field(document, "firstName"), + text_field(document, "middleName"), + last_name, + ) + }) + }) + .collect(); + expected.sort(); + assert!(!expected.is_empty(), "fixture should produce matches"); + + let returned: Vec<(String, String, String)> = documents + .iter() + .map(|document| { + ( + text_field(document, "firstName"), + text_field(document, "middleName"), + text_field(document, "lastName"), + ) + }) + .collect(); + assert_eq!(returned, expected); + } + + #[test] + fn test_two_in_clauses_with_descending_left_over_property() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup_family_tests(10, 73509, platform_version); + let people = all_people(&drive, &contract, platform_version); + + let names_of = |field: &str| -> Vec { + let mut names: Vec = people + .iter() + .map(|(_, document)| text_field(document, field)) + .collect(); + names.sort(); + names.dedup(); + names + }; + let first_names = names_of("firstName"); + let middle_names = names_of("middleName"); + + // [firstName, middleName, lastName]: lastName is left over and + // ordered descending + let query_value = json!({ + "where": [ + ["firstName", "in", first_names], + ["middleName", "in", middle_names], + ], + "limit": 100, + "orderBy": [ + ["firstName", "asc"], + ["middleName", "asc"], + ["lastName", "desc"], + ] + }); + let documents = + run_query_with_proof_round_trip(&drive, &contract, query_value, platform_version); + + let mut expected: Vec<(String, String, std::cmp::Reverse)> = people + .iter() + .map(|(_, document)| { + ( + text_field(document, "firstName"), + text_field(document, "middleName"), + std::cmp::Reverse(text_field(document, "lastName")), + ) + }) + .collect(); + expected.sort(); + + let returned: Vec<(String, String, std::cmp::Reverse)> = documents + .iter() + .map(|document| { + ( + text_field(document, "firstName"), + text_field(document, "middleName"), + std::cmp::Reverse(text_field(document, "lastName")), + ) + }) + .collect(); + assert_eq!(returned, expected); + assert_eq!(returned.len(), 10); + } + + #[test] + fn test_equality_prefix_with_two_in_clauses() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup_family_tests(10, 73509, platform_version); + let people = all_people(&drive, &contract, platform_version); + + // index: [age, firstName, middleName, lastName] + let age = people[0] + .1 + .get("age") + .expect("age should exist") + .to_integer::() + .expect("age should be an integer"); + let names_of = |field: &str| -> Vec { + let mut names: Vec = people + .iter() + .map(|(_, document)| text_field(document, field)) + .collect(); + names.sort(); + names.dedup(); + names + }; + let first_names = names_of("firstName"); + let middle_names = names_of("middleName"); + + let query_value = json!({ + "where": [ + ["age", "==", age], + ["firstName", "in", first_names], + ["middleName", "in", middle_names], + ], + "limit": 100, + "orderBy": [ + ["firstName", "asc"], + ["middleName", "asc"], + ] + }); + let documents = + run_query_with_proof_round_trip(&drive, &contract, query_value, platform_version); + + let mut expected: Vec<(String, String)> = people + .iter() + .filter_map(|(_, document)| { + let document_age = document + .get("age") + .expect("age should exist") + .to_integer::() + .expect("age should be an integer"); + (document_age == age).then(|| { + ( + text_field(document, "firstName"), + text_field(document, "middleName"), + ) + }) + }) + .collect(); + expected.sort(); + assert!(!expected.is_empty(), "fixture should produce matches"); + + let returned: Vec<(String, String)> = documents + .iter() + .map(|document| { + ( + text_field(document, "firstName"), + text_field(document, "middleName"), + ) + }) + .collect(); + assert_eq!(returned, expected); + } + + #[test] + fn test_multiple_in_clauses_rejected_before_protocol_version_14() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup_family_tests(10, 73509, platform_version); + let platform_version_13 = + PlatformVersion::get(13).expect("protocol version 13 should exist"); + + let person_document_type = contract + .document_type_for_name("person") + .expect("contract should have a person document type"); + let query_value = json!({ + "where": [ + ["firstName", "in", ["Adey", "Briney"]], + ["lastName", "in", ["Kriskov", "Randolf"]], + ], + "limit": 100, + "orderBy": [ + ["firstName", "asc"], + ["lastName", "asc"], + ] + }); + let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None) + .expect("expected to serialize to cbor"); + // The grammar groups multiple in clauses structurally + let query = DriveDocumentQuery::from_cbor( + where_cbor.as_slice(), + &contract, + person_document_type, + &drive.config, + platform_version, + ) + .expect("query should be built"); + + // ... but the v0 (protocol version <= 13) lowering rejects them + let error = query + .execute_raw_results_no_proof(&drive, None, None, platform_version_13) + .expect_err("multiple in clauses must be rejected before protocol version 14"); + assert!( + matches!(error, Error::Query(QuerySyntaxError::MultipleInClauses(_))), + "expected MultipleInClauses, got {error:?}" + ); + + let error = query + .clone() + .execute_with_proof(&drive, None, None, platform_version_13) + .expect_err("multiple in clauses must be rejected on the proof path too"); + assert!( + matches!(error, Error::Query(QuerySyntaxError::MultipleInClauses(_))), + "expected MultipleInClauses, got {error:?}" + ); + + // ... and protocol version 14 accepts the very same query + query + .execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect("the same query should execute at protocol version 14"); + } + + #[test] + fn test_multiple_in_clauses_reject_cursor_pagination() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup_family_tests(10, 73509, platform_version); + let people = all_people(&drive, &contract, platform_version); + let start_after = people[0].1.id().to_string(Encoding::Base58); + + let person_document_type = contract + .document_type_for_name("person") + .expect("contract should have a person document type"); + let query_value = json!({ + "where": [ + ["firstName", "in", ["Adey", "Briney"]], + ["lastName", "in", ["Kriskov", "Randolf"]], + ], + "startAfter": start_after, + "limit": 100, + "orderBy": [ + ["firstName", "asc"], + ["lastName", "asc"], + ] + }); + let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None) + .expect("expected to serialize to cbor"); + let query = DriveDocumentQuery::from_cbor( + where_cbor.as_slice(), + &contract, + person_document_type, + &drive.config, + platform_version, + ) + .expect("query should be built"); + let error = query + .execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect_err("cursor pagination with multiple in clauses must be rejected"); + assert!( + matches!(error, Error::Query(QuerySyntaxError::Unsupported(_))), + "expected Unsupported, got {error:?}" + ); + } + + #[test] + fn test_multiple_in_clauses_cursor_rejection_precedes_cursor_lookup() { + // The shape preflight must fire before the startAfter document is + // fetched from storage: a nonexistent cursor may not surface as + // StartDocumentNotFound when the shape itself is unsupported (v14) + // or rejected (v13). + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup_family_tests(10, 73509, platform_version); + let platform_version_13 = + PlatformVersion::get(13).expect("protocol version 13 should exist"); + + let person_document_type = contract + .document_type_for_name("person") + .expect("contract should have a person document type"); + let nonexistent = Identifier::from([77u8; 32]).to_string(Encoding::Base58); + let query_value = json!({ + "where": [ + ["firstName", "in", ["Adey", "Briney"]], + ["lastName", "in", ["Kriskov", "Randolf"]], + ], + "startAfter": nonexistent, + "limit": 100, + "orderBy": [ + ["firstName", "asc"], + ["lastName", "asc"], + ] + }); + let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None) + .expect("expected to serialize to cbor"); + let query = DriveDocumentQuery::from_cbor( + where_cbor.as_slice(), + &contract, + person_document_type, + &drive.config, + platform_version, + ) + .expect("query should be built"); + + let error = query + .execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect_err("multi-in with a cursor must be rejected at protocol version 14"); + assert!( + matches!(error, Error::Query(QuerySyntaxError::Unsupported(_))), + "expected Unsupported before the cursor lookup, got {error:?}" + ); + + let error = query + .execute_raw_results_no_proof(&drive, None, None, platform_version_13) + .expect_err("multi-in must be rejected at protocol version 13"); + assert!( + matches!(error, Error::Query(QuerySyntaxError::MultipleInClauses(_))), + "expected MultipleInClauses before the cursor lookup, got {error:?}" + ); + } + + #[test] + fn test_multiple_in_clauses_cross_product_cap() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup_family_tests(10, 73509, platform_version); + + let person_document_type = contract + .document_type_for_name("person") + .expect("contract should have a person document type"); + let first_names: Vec = (0..20).map(|i| format!("First{i:02}")).collect(); + let last_names: Vec = (0..6).map(|i| format!("Last{i}")).collect(); + let query_value = json!({ + "where": [ + ["firstName", "in", first_names], + ["lastName", "in", last_names], + ], + "limit": 100, + "orderBy": [ + ["firstName", "asc"], + ["lastName", "asc"], + ] + }); + let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None) + .expect("expected to serialize to cbor"); + let query = DriveDocumentQuery::from_cbor( + where_cbor.as_slice(), + &contract, + person_document_type, + &drive.config, + platform_version, + ) + .expect("query should be built"); + let error = query + .execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect_err("a 120-branch cross product must be rejected"); + assert!( + matches!(error, Error::Query(QuerySyntaxError::InvalidInClause(_))), + "expected InvalidInClause, got {error:?}" + ); + } + + #[test] + fn test_multiple_in_clauses_must_be_consecutive_index_properties() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup_family_tests(10, 73509, platform_version); + + let person_document_type = contract + .document_type_for_name("person") + .expect("contract should have a person document type"); + // No index has middleName and lastName as a leading consecutive + // run: [firstName, middleName, lastName] holds them at positions + // 1 and 2 with no equality on firstName. + let query_value = json!({ + "where": [ + ["middleName", "in", ["Ivanna", "Evangeline"]], + ["lastName", "in", ["Kriskov", "Randolf"]], + ], + "limit": 100, + "orderBy": [ + ["middleName", "asc"], + ["lastName", "asc"], + ] + }); + let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None) + .expect("expected to serialize to cbor"); + let query = DriveDocumentQuery::from_cbor( + where_cbor.as_slice(), + &contract, + person_document_type, + &drive.config, + platform_version, + ) + .expect("query should be built"); + let error = query + .execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect_err("non-consecutive in clauses must be rejected"); + assert!( + matches!( + error, + Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(_)) + ), + "expected WhereClauseOnNonIndexedProperty, got {error:?}" + ); + } + + #[test] + fn test_multiple_in_clauses_require_order_by_on_each_in_field() { + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup_family_tests(10, 73509, platform_version); + + let person_document_type = contract + .document_type_for_name("person") + .expect("contract should have a person document type"); + let query_value = json!({ + "where": [ + ["firstName", "in", ["Adey", "Briney"]], + ["lastName", "in", ["Kriskov", "Randolf"]], + ], + "limit": 100, + "orderBy": [ + ["firstName", "asc"], + ] + }); + let where_cbor = cbor_serializer::serializable_value_to_cbor(&query_value, None) + .expect("expected to serialize to cbor"); + let query = DriveDocumentQuery::from_cbor( + where_cbor.as_slice(), + &contract, + person_document_type, + &drive.config, + platform_version, + ) + .expect("query should be built"); + let error = query + .execute_raw_results_no_proof(&drive, None, None, platform_version) + .expect_err("missing order by on an in field must be rejected"); + // Index selection rejects the shape first: the order-by continuity + // rule in `Index::matches` disqualifies every candidate index before + // the per-field `MissingOrderByForRange` guard could fire + assert!( + matches!( + error, + Error::Query(QuerySyntaxError::WhereClauseOnNonIndexedProperty(_)) + ), + "expected WhereClauseOnNonIndexedProperty, got {error:?}" + ); + } +} + +#[cfg(feature = "server")] +#[cfg(test)] +mod withdrawal_in_clause_placement_equivalence { + //! Pins that the v0 (protocol version <= 13) and v1 (protocol version + //! 14) withdrawal-by-transaction-index query builders are + //! behavior-preserving twins: the `In` clause riding in `equal_clauses` + //! (v0) and in `in_clauses` (v1) lower to the identical grovedb path + //! query at both protocol versions, so withdrawal processing executes + //! the same operations at the same cost across the version flip. + + use super::*; + use dpp::data_contracts::SystemDataContract; + use dpp::system_data_contracts::load_system_data_contract; + use drive::query::{InternalClauses, OrderClause}; + use indexmap::IndexMap; + + #[test] + fn test_in_clause_bucket_placement_lowers_identically() { + let platform_version = PlatformVersion::latest(); + let contract = load_system_data_contract(SystemDataContract::Withdrawals, platform_version) + .expect("expected to load withdrawals contract"); + let document_type = contract + .document_type_for_name("withdrawal") + .expect("expected withdrawal document type"); + + let status_clause = WhereClause { + field: "status".to_string(), + operator: WhereOperator::Equal, + value: Value::U8(1), + }; + let transaction_index_clause = WhereClause { + field: "transactionIndex".to_string(), + operator: WhereOperator::In, + value: Value::Array(vec![Value::U64(1), Value::U64(3), Value::U64(7)]), + }; + let mut order_by = IndexMap::new(); + order_by.insert( + "transactionIndex".to_string(), + OrderClause { + field: "transactionIndex".to_string(), + ascending: true, + }, + ); + + // The pre-change shape: the In clause rode in equal_clauses + let mut equal_clauses_with_in = BTreeMap::new(); + equal_clauses_with_in.insert("status".to_string(), status_clause.clone()); + equal_clauses_with_in.insert( + "transactionIndex".to_string(), + transaction_index_clause.clone(), + ); + let legacy_shape = drive::query::DriveDocumentQuery { + contract: &contract, + document_type, + internal_clauses: InternalClauses { + primary_key_in_clause: None, + primary_key_equal_clause: None, + in_clauses: Vec::new(), + range_clause: None, + equal_clauses: equal_clauses_with_in, + }, + offset: None, + limit: Some(16), + order_by: order_by.clone(), + start_at: None, + start_at_included: false, + block_time_ms: None, + }; + + // The current shape: the In clause in in_clauses + let mut equal_clauses = BTreeMap::new(); + equal_clauses.insert("status".to_string(), status_clause); + let current_shape = drive::query::DriveDocumentQuery { + contract: &contract, + document_type, + internal_clauses: InternalClauses { + primary_key_in_clause: None, + primary_key_equal_clause: None, + in_clauses: vec![transaction_index_clause], + range_clause: None, + equal_clauses, + }, + offset: None, + limit: Some(16), + order_by, + start_at: None, + start_at_included: false, + block_time_ms: None, + }; + + for protocol_version in [13u32, 14u32] { + let version = + PlatformVersion::get(protocol_version).expect("expected platform version to exist"); + let legacy_path_query = legacy_shape + .construct_path_query(None, version) + .expect("legacy shape should lower"); + let current_path_query = current_shape + .construct_path_query(None, version) + .expect("current shape should lower"); + assert_eq!( + legacy_path_query, current_path_query, + "the two in-clause bucket placements must lower to the identical \ + path query at protocol version {protocol_version}" + ); + } + } +} diff --git a/packages/rs-drive/tests/query_tests_history.rs b/packages/rs-drive/tests/query_tests_history.rs index 897dd668936..a92fad69c74 100644 --- a/packages/rs-drive/tests/query_tests_history.rs +++ b/packages/rs-drive/tests/query_tests_history.rs @@ -313,6 +313,7 @@ fn test_query_historical_first_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -562,6 +563,7 @@ fn test_query_historical_first_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -612,6 +614,7 @@ fn test_query_historical_first_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -658,6 +661,7 @@ fn test_query_historical_first_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -738,6 +742,7 @@ fn test_query_historical_first_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -792,6 +797,7 @@ fn test_query_historical_first_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -840,6 +846,7 @@ fn test_query_historical_first_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -882,6 +889,7 @@ fn test_query_historical_first_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -957,6 +965,7 @@ fn test_query_historical_first_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -1011,6 +1020,7 @@ fn test_query_historical_first_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -1704,6 +1714,7 @@ fn test_query_historical_latest_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -1953,6 +1964,7 @@ fn test_query_historical_latest_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -2003,6 +2015,7 @@ fn test_query_historical_latest_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -2049,6 +2062,7 @@ fn test_query_historical_latest_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -2129,6 +2143,7 @@ fn test_query_historical_latest_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -2183,6 +2198,7 @@ fn test_query_historical_latest_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -2231,6 +2247,7 @@ fn test_query_historical_latest_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -2273,6 +2290,7 @@ fn test_query_historical_latest_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -2348,6 +2366,7 @@ fn test_query_historical_latest_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query @@ -2402,6 +2421,7 @@ fn test_query_historical_latest_platform_version() { &contract, person_document_type, &drive.config, + platform_version, ) .expect("query should be built"); let (results, _, _) = query diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs index 7b152be0f71..e7280615772 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/mod.rs @@ -41,6 +41,22 @@ pub struct DriveDocumentQueryMethodVersions { /// versions; the routing itself is unreachable before the ranked /// contract grammar activates. pub detect_ranked_mode: FeatureVersion, + /// Lowering of a `DriveDocumentQuery` over a secondary index into a + /// grovedb `PathQuery`. Versioned because the set of accepted query + /// shapes is part of the consensus query contract: v0 rejects more + /// than one non-primary-key `In` clause per query; v1 (protocol + /// version 14) accepts multiple `In` clauses on consecutive index + /// properties, lowering them to multi-level key-set path queries. + pub non_primary_key_path_query: FeatureVersion, + /// Grouping of a query's raw where clauses into equality / range / + /// in buckets (`WhereClause::group_clauses`). Versioned because the + /// error surface for rejected shapes is part of the query contract: + /// v0 rejects any query with more than one non-primary-key `In` + /// clause with `MultipleInClauses` before any other same-field + /// checks; v1 (protocol version 14) groups multiple `In` clauses + /// structurally and reports duplicate-field and overlap errors + /// instead. Flips together with `non_primary_key_path_query`. + pub where_clause_grouping: FeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs index dc32d71f710..004078759dd 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v1.rs @@ -18,6 +18,8 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V1: DriveDocumentMethodVersions = detect_count_mode: 0, detect_sum_mode: 0, detect_ranked_mode: 0, + non_primary_key_path_query: 0, + where_clause_grouping: 0, }, delete: DriveDocumentDeleteMethodVersions { add_estimation_costs_for_remove_document_to_primary_storage: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs index 6fcfc1b5dca..f1cfbe10f40 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v2.rs @@ -20,6 +20,8 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V2: DriveDocumentMethodVersions = detect_count_mode: 0, detect_sum_mode: 0, detect_ranked_mode: 0, + non_primary_key_path_query: 0, + where_clause_grouping: 0, }, delete: DriveDocumentDeleteMethodVersions { add_estimation_costs_for_remove_document_to_primary_storage: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v3.rs index 823842c8c01..31966e7f684 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v3.rs @@ -30,6 +30,8 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V3: DriveDocumentMethodVersions = detect_count_mode: 0, detect_sum_mode: 0, detect_ranked_mode: 0, + non_primary_key_path_query: 0, + where_clause_grouping: 0, }, delete: DriveDocumentDeleteMethodVersions { add_estimation_costs_for_remove_document_to_primary_storage: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v4.rs index 5993ebac2a9..b57236bac37 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_document_method_versions/v4.rs @@ -5,8 +5,11 @@ use crate::version::drive_versions::drive_document_method_versions::{ DriveDocumentQueryMethodVersions, DriveDocumentUpdateMethodVersions, }; -/// V4 is protocol version 14's document-method table. It hosts two -/// independent changes that both gate at v14. +/// V4 is protocol version 14's document-method table. It hosts three +/// independent changes that all gate at v14 (ranked aggregates, the +/// shared-prefix aggregate index fix, and multi-`In` document queries +/// via `query.non_primary_key_path_query: 1` — v13 and earlier keep +/// the v0 lowering, which rejects more than one `In` clause). /// /// ## 1. Contract-level ranked aggregates /// @@ -71,6 +74,8 @@ pub const DRIVE_DOCUMENT_METHOD_VERSIONS_V4: DriveDocumentMethodVersions = detect_count_mode: 0, detect_sum_mode: 0, detect_ranked_mode: 0, + non_primary_key_path_query: 1, + where_clause_grouping: 1, }, delete: DriveDocumentDeleteMethodVersions { add_estimation_costs_for_remove_document_to_primary_storage: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_identity_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_identity_method_versions/mod.rs index a701944c844..6ea2f0a325d 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_identity_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_identity_method_versions/mod.rs @@ -1,6 +1,7 @@ use versioned_feature_core::{FeatureVersion, OptionalFeatureVersion}; pub mod v1; +pub mod v2; #[derive(Clone, Debug, Default)] pub struct DriveIdentityMethodVersions { diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_identity_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_identity_method_versions/v2.rs new file mode 100644 index 00000000000..260810a4d1c --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_versions/drive_identity_method_versions/v2.rs @@ -0,0 +1,172 @@ +use crate::version::drive_versions::drive_identity_method_versions::{ + DriveIdentityContractInfoMethodVersions, DriveIdentityCostEstimationMethodVersions, + DriveIdentityFetchAttributesMethodVersions, DriveIdentityFetchFullIdentityMethodVersions, + DriveIdentityFetchMethodVersions, DriveIdentityFetchPartialIdentityMethodVersions, + DriveIdentityFetchPublicKeyHashesMethodVersions, DriveIdentityInsertMethodVersions, + DriveIdentityKeyHashesToIdentityInsertMethodVersions, DriveIdentityKeysFetchMethodVersions, + DriveIdentityKeysInsertMethodVersions, DriveIdentityKeysMethodVersions, + DriveIdentityKeysProveMethodVersions, DriveIdentityMethodVersions, + DriveIdentityProveMethodVersions, DriveIdentityUpdateMethodVersions, + DriveIdentityWithdrawalDocumentMethodVersions, DriveIdentityWithdrawalMethodVersions, + DriveIdentityWithdrawalTransactionIndexMethodVersions, + DriveIdentityWithdrawalTransactionMethodVersions, + DriveIdentityWithdrawalTransactionQueueMethodVersions, +}; + +/// V2 is protocol version 14's identity-method table. It differs from V1 +/// in exactly one flip: +/// `withdrawals.document.find_withdrawal_documents_by_status_and_transaction_indices` +/// 0 -> 1, selecting the v1 withdrawal-by-transaction-index query builder +/// that carries the transaction-index `In` clause in +/// `InternalClauses.in_clauses` instead of smuggling it through +/// `equal_clauses`. The two builders lower to the identical grovedb path +/// query (pinned by +/// `withdrawal_in_clause_placement_equivalence` in rs-drive's +/// query tests), so this is a structural version bump, not a behavior +/// change; v0 stays byte-frozen for protocol versions up to 13. +pub const DRIVE_IDENTITY_METHOD_VERSIONS_V2: DriveIdentityMethodVersions = + DriveIdentityMethodVersions { + fetch: DriveIdentityFetchMethodVersions { + public_key_hashes: DriveIdentityFetchPublicKeyHashesMethodVersions { + fetch_full_identities_by_unique_public_key_hashes: 0, + fetch_full_identity_by_unique_public_key_hash: 0, + fetch_identity_id_by_unique_public_key_hash: 0, + fetch_identity_ids_by_non_unique_public_key_hash: 0, + fetch_identity_ids_by_unique_public_key_hashes: 0, + fetch_serialized_full_identity_by_unique_public_key_hash: 0, + has_any_of_unique_public_key_hashes: 0, + has_non_unique_public_key_hash: 0, + has_non_unique_public_key_hash_already_for_identity: 0, + has_unique_public_key_hash: 0, + fetch_full_identity_by_non_unique_public_key_hash: 0, + }, + attributes: DriveIdentityFetchAttributesMethodVersions { + revision: 0, + nonce: 0, + identity_contract_nonce: 0, + balance: 0, + balance_include_debt: 0, + negative_balance: 0, + }, + partial_identity: DriveIdentityFetchPartialIdentityMethodVersions { + fetch_identity_revision_with_keys: 0, + fetch_identity_balance_with_keys: 0, + fetch_identity_balance_with_keys_and_revision: 0, + fetch_identity_with_balance: 0, + fetch_identity_keys: 0, + }, + full_identity: DriveIdentityFetchFullIdentityMethodVersions { + fetch_full_identity: Some(0), + fetch_full_identities: Some(0), + }, + }, + prove: DriveIdentityProveMethodVersions { + full_identity: 0, + full_identities: 0, + identity_nonce: 0, + identity_contract_nonce: 0, + identities_contract_keys: 0, + prove_full_identities_by_unique_public_key_hashes: 0, + prove_full_identity_by_unique_public_key_hash: 0, + prove_identity_id_by_unique_public_key_hash: 0, + prove_identity_ids_by_unique_public_key_hashes: 0, + prove_full_identity_by_non_unique_public_key_hash: 0, + }, + keys: DriveIdentityKeysMethodVersions { + fetch: DriveIdentityKeysFetchMethodVersions { + fetch_all_current_identity_keys: 0, + fetch_all_identity_keys: 0, + fetch_identities_all_keys: 0, + fetch_identity_keys: 0, + fetch_identities_contract_keys: 0, + }, + prove: DriveIdentityKeysProveMethodVersions { + prove_identities_all_keys: 0, + prove_identity_keys: 0, + }, + insert: DriveIdentityKeysInsertMethodVersions { + create_key_tree_with_keys: 0, + create_new_identity_key_query_trees: 0, + insert_key_searchable_references: 0, + insert_key_to_storage: 0, + insert_new_non_unique_key: 0, + insert_new_unique_key: 0, + replace_key_in_storage: 0, + }, + insert_key_hash_identity_reference: + DriveIdentityKeyHashesToIdentityInsertMethodVersions { + add_estimation_costs_for_insert_non_unique_public_key_hash_reference: 0, + add_estimation_costs_for_insert_unique_public_key_hash_reference: 0, + insert_non_unique_public_key_hash_reference_to_identity: 0, + insert_reference_to_non_unique_key: 0, + insert_reference_to_unique_key: 0, + insert_unique_public_key_hash_reference_to_identity: 0, + }, + }, + update: DriveIdentityUpdateMethodVersions { + update_identity_revision: 0, + merge_identity_nonce: 0, + update_identity_negative_credit_operation: 0, + initialize_identity_revision: 0, + disable_identity_keys: 0, + re_enable_identity_keys: 0, + add_new_non_unique_keys_to_identity: 0, + add_new_unique_keys_to_identity: 0, + add_new_keys_to_identity: 0, + insert_identity_balance: 0, + initialize_negative_identity_balance: 0, + add_to_identity_balance: 0, + add_to_previous_balance: 0, + apply_balance_change_from_fee_to_identity: 0, + remove_from_identity_balance: 0, + refresh_identity_key_reference_operations: 0, + }, + insert: DriveIdentityInsertMethodVersions { + add_new_identity: 0, + }, + contract_info: DriveIdentityContractInfoMethodVersions { + add_potential_contract_info_for_contract_bounded_key: 0, + refresh_potential_contract_info_key_references: 0, + merge_identity_contract_nonce: 0, + }, + cost_estimation: DriveIdentityCostEstimationMethodVersions { + for_authentication_keys_security_level_in_key_reference_tree: 0, + for_balances: 0, + for_token_balances: 0, + for_token_total_supply: 0, + for_contract_info: 0, + for_contract_info_group: 0, + for_contract_info_group_keys: 0, + for_contract_info_group_key_purpose: 0, + for_keys_for_identity_id: 0, + for_negative_credit: 0, + for_purpose_in_key_reference_tree: 0, + for_root_key_reference_tree: 0, + for_update_revision: 0, + for_token_identity_infos: 0, + for_token_perpetual_distribution: 0, + for_token_pre_programmed_distribution: 0, + for_root_token_ms_interval_distribution: 0, + for_token_selling_prices: 0, + for_token_contract_infos: 0, + }, + withdrawals: DriveIdentityWithdrawalMethodVersions { + document: DriveIdentityWithdrawalDocumentMethodVersions { + fetch_oldest_withdrawal_documents_by_status: 0, + find_withdrawal_documents_by_status_and_transaction_indices: 1, + }, + transaction: DriveIdentityWithdrawalTransactionMethodVersions { + index: DriveIdentityWithdrawalTransactionIndexMethodVersions { + fetch_next_withdrawal_transaction_index: 0, + add_update_next_withdrawal_transaction_index_operation: 0, + }, + queue: DriveIdentityWithdrawalTransactionQueueMethodVersions { + add_enqueue_untied_withdrawal_transaction_operations: 0, + dequeue_untied_withdrawal_transactions: 0, + remove_broadcasted_withdrawal_transactions_after_completion_operations: 0, + move_broadcasted_withdrawal_transactions_back_to_queue_operations: 0, + }, + }, + calculate_current_withdrawal_limit: 0, + }, + }; diff --git a/packages/rs-platform-version/src/version/drive_versions/v9.rs b/packages/rs-platform-version/src/version/drive_versions/v9.rs index b01afe5816d..54f4e357c57 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v9.rs @@ -5,7 +5,7 @@ use crate::version::drive_versions::drive_document_method_versions::v4::DRIVE_DO use crate::version::drive_versions::drive_group_method_versions::v1::DRIVE_GROUP_METHOD_VERSIONS_V1; use crate::version::drive_versions::drive_group_method_versions::DriveShieldedMethodVersions; use crate::version::drive_versions::drive_grove_method_versions::v1::DRIVE_GROVE_METHOD_VERSIONS_V1; -use crate::version::drive_versions::drive_identity_method_versions::v1::DRIVE_IDENTITY_METHOD_VERSIONS_V1; +use crate::version::drive_versions::drive_identity_method_versions::v2::DRIVE_IDENTITY_METHOD_VERSIONS_V2; use crate::version::drive_versions::drive_state_transition_method_versions::v3::DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V3; use crate::version::drive_versions::drive_structure_version::v1::DRIVE_STRUCTURE_V1; use crate::version::drive_versions::drive_token_method_versions::v1::DRIVE_TOKEN_METHOD_VERSIONS_V1; @@ -82,7 +82,7 @@ pub const DRIVE_VERSION_V9: DriveVersion = DriveVersion { fetch_asset_lock_outpoint_info: 0, }, verify: DRIVE_VERIFY_METHOD_VERSIONS_V2, // changed in v8: compacted address-balance proof envelope (verify v1) - identity: DRIVE_IDENTITY_METHOD_VERSIONS_V1, + identity: DRIVE_IDENTITY_METHOD_VERSIONS_V2, // changed in v9: v1 withdrawal-by-transaction-index query builder (structural, identical lowering) token: DRIVE_TOKEN_METHOD_VERSIONS_V1, platform_system: DrivePlatformSystemMethodVersions { estimation_costs: DriveSystemEstimationCostsMethodVersions { diff --git a/packages/rs-sdk/src/platform/documents/document_query.rs b/packages/rs-sdk/src/platform/documents/document_query.rs index 57648cd1988..ebf0a3a0dbf 100644 --- a/packages/rs-sdk/src/platform/documents/document_query.rs +++ b/packages/rs-sdk/src/platform/documents/document_query.rs @@ -783,8 +783,14 @@ impl<'a> TryFrom<&'a DocumentQuery> for DriveDocumentQuery<'a> { .document_type_for_name(&request.document_type_name) .map_err(ProtocolError::DataContractError)?; - let internal_clauses = InternalClauses::extract_from_clauses(request.where_clauses.clone()) - .map_err(Error::Drive)?; + // Client-side construction groups under the latest grammar; the + // server and the proof verifier enforce the network's protocol + // version at path-query lowering. + let internal_clauses = InternalClauses::extract_from_clauses( + request.where_clauses.clone(), + PlatformVersion::latest(), + ) + .map_err(Error::Drive)?; let limit = if request.limit != 0 { Some(request.limit as u16) diff --git a/packages/wasm-drive-verify/src/document/verify_proof.rs b/packages/wasm-drive-verify/src/document/verify_proof.rs index 02cc5ac5e9d..b3cbfbbf726 100644 --- a/packages/wasm-drive-verify/src/document/verify_proof.rs +++ b/packages/wasm-drive-verify/src/document/verify_proof.rs @@ -156,10 +156,30 @@ fn parse_internal_clauses(where_clauses: &JsValue) -> Result Result Result