@@ -39,37 +39,76 @@ pub struct LocalDbOrderTrade {
3939
4040const ORDER_HASH_CLAUSE : & str = "/*ORDER_HASH_CLAUSE*/" ;
4141const ORDER_HASH_LIST_BODY : & str = "AND tws.order_hash IN ({list})" ;
42+ /// Match-NONE predicate emitted for an empty `These` filter. SQLite rejects the
43+ /// degenerate `IN ()` form, so we splice a constant-false predicate that the
44+ /// query optimizer prunes to zero rows. This makes "filter to exactly these
45+ /// (none) hashes" return no rows — the deliberate opposite of `All`.
46+ const ORDER_HASH_MATCH_NONE_BODY : & str = "AND 1=0" ;
4247
4348const START_TS_CLAUSE : & str = "/*START_TS_CLAUSE*/" ;
4449const START_TS_BODY : & str = "\n AND tws.block_timestamp >= {param}\n " ;
4550
4651const END_TS_CLAUSE : & str = "/*END_TS_CLAUSE*/" ;
4752const END_TS_BODY : & str = "\n AND tws.block_timestamp <= {param}\n " ;
4853
54+ /// Explicit selection of which orders' trades a fetch covers, so the builder
55+ /// never has to read "all" out of an empty slice.
56+ ///
57+ /// - [`OrderHashFilter::All`] emits no order-hash predicate at all, so every
58+ /// order's trades for the chain/raindex (within the optional time window) are
59+ /// returned.
60+ /// - [`OrderHashFilter::These`] filters to exactly the given hashes via
61+ /// `WHERE order_hash IN (...)`. An empty slice means *none*: it emits a
62+ /// match-NONE predicate and returns zero rows. It is the deliberate opposite
63+ /// of `All`, not a synonym for it.
64+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
65+ pub enum OrderHashFilter < ' a > {
66+ /// No order-hash predicate: trades for every order are returned.
67+ All ,
68+ /// Trades for exactly these order hashes. Empty = none (zero rows), never
69+ /// all.
70+ These ( & ' a [ B256 ] ) ,
71+ }
72+
4973/// Builds the SQL statement for retrieving order trades within the specified
50- /// window. Accepts a slice of order hashes and emits a single query with a
51- /// `WHERE order_hash IN (...)` clause, so trades for one or many orders are
52- /// fetched in a single query (eliminating the N+1 query pattern and per-query
53- /// connection overhead). The single-order path passes a one-element slice .
74+ /// window. The `filter` explicitly selects which orders are covered, so trades
75+ /// for one or many orders are fetched in a single query (eliminating the N+1
76+ /// query pattern and per-query connection overhead). The single-order path
77+ /// passes `These(&[hash])` .
5478///
55- /// When `order_hashes` is empty the order-hash clause is removed entirely, so
56- /// the query degenerates to "all trades for this chain/raindex" within the
57- /// optional time window. Callers that want an empty result for an empty input
58- /// should short-circuit before invoking this builder.
79+ /// The order-hash predicate rendered depends on `filter`:
80+ /// - [`OrderHashFilter::All`] => no order-hash predicate (every order).
81+ /// - [`OrderHashFilter::These`] non-empty => `AND order_hash IN (...)`.
82+ /// - [`OrderHashFilter::These`] empty => `AND 1=0` (match nothing): an empty
83+ /// `These` is *none*, never all. SQLite rejects `IN ()`, so the constant-false
84+ /// predicate stands in for it.
5985pub fn build_fetch_order_trades_batch_stmt (
6086 raindex_id : & RaindexIdentifier ,
61- order_hashes : & [ B256 ] ,
87+ filter : OrderHashFilter < ' _ > ,
6288 start_timestamp : Option < u64 > ,
6389 end_timestamp : Option < u64 > ,
6490) -> Result < SqlStatement , SqlBuildError > {
6591 let mut stmt = SqlStatement :: new ( QUERY_TEMPLATE ) ;
6692 stmt. push ( SqlValue :: from ( raindex_id. chain_id ) ) ;
6793 stmt. push ( SqlValue :: from ( raindex_id. raindex_address ) ) ;
68- stmt. bind_list_clause (
69- ORDER_HASH_CLAUSE ,
70- ORDER_HASH_LIST_BODY ,
71- order_hashes. iter ( ) . copied ( ) . map ( SqlValue :: from) ,
72- ) ?;
94+ match filter {
95+ OrderHashFilter :: All => {
96+ // No order-hash predicate: drop the marker, keep all orders.
97+ stmt. replace ( ORDER_HASH_CLAUSE , "" ) ?;
98+ }
99+ OrderHashFilter :: These ( [ ] ) => {
100+ // Empty `These` means none. `IN ()` is invalid in SQLite, so splice
101+ // a constant-false predicate that yields zero rows.
102+ stmt. replace ( ORDER_HASH_CLAUSE , ORDER_HASH_MATCH_NONE_BODY ) ?;
103+ }
104+ OrderHashFilter :: These ( hashes) => {
105+ stmt. bind_list_clause (
106+ ORDER_HASH_CLAUSE ,
107+ ORDER_HASH_LIST_BODY ,
108+ hashes. iter ( ) . copied ( ) . map ( SqlValue :: from) ,
109+ ) ?;
110+ }
111+ }
73112
74113 // Optional time filters
75114 let start_param = if let Some ( v) = start_timestamp {
@@ -112,7 +151,7 @@ mod tests {
112151 let hash_b = b256 ! ( "0x00000000000000000000000000000000000000000000000000000000deadface" ) ;
113152 let stmt = build_fetch_order_trades_batch_stmt (
114153 & RaindexIdentifier :: new ( 137 , Address :: ZERO ) ,
115- & [ hash_a, hash_b] ,
154+ OrderHashFilter :: These ( & [ hash_a, hash_b] ) ,
116155 Some ( 11 ) ,
117156 Some ( 22 ) ,
118157 )
@@ -144,7 +183,7 @@ mod tests {
144183 let hash = b256 ! ( "0x00000000000000000000000000000000000000000000000000000000deadbeef" ) ;
145184 let stmt = build_fetch_order_trades_batch_stmt (
146185 & RaindexIdentifier :: new ( 1 , Address :: ZERO ) ,
147- & [ hash] ,
186+ OrderHashFilter :: These ( & [ hash] ) ,
148187 None ,
149188 None ,
150189 )
@@ -170,7 +209,7 @@ mod tests {
170209 let hash = b256 ! ( "0x00000000000000000000000000000000000000000000000000000000deadface" ) ;
171210 let stmt = build_fetch_order_trades_batch_stmt (
172211 & RaindexIdentifier :: new ( 137 , Address :: ZERO ) ,
173- & [ hash] ,
212+ OrderHashFilter :: These ( & [ hash] ) ,
174213 Some ( 11 ) ,
175214 Some ( 22 ) ,
176215 )
@@ -193,22 +232,56 @@ mod tests {
193232 }
194233
195234 #[ test]
196- fn batch_empty_hashes_drops_order_hash_clause ( ) {
235+ fn all_emits_no_order_hash_predicate ( ) {
236+ // `All` is the only variant that drops the order-hash predicate: no IN
237+ // list and no match-NONE constant. Every order is returned.
238+ let stmt = build_fetch_order_trades_batch_stmt (
239+ & RaindexIdentifier :: new ( 1 , Address :: ZERO ) ,
240+ OrderHashFilter :: All ,
241+ None ,
242+ None ,
243+ )
244+ . unwrap ( ) ;
245+
246+ // The marker is consumed and *no* order-hash predicate is rendered (the
247+ // SELECT list still projects tws.order_hash, so only the predicate forms
248+ // are asserted absent).
249+ assert ! ( !stmt. sql. contains( ORDER_HASH_CLAUSE ) ) ;
250+ assert ! ( !stmt. sql. contains( "tws.order_hash IN (" ) ) ;
251+ assert ! ( !stmt. sql. contains( "tws.order_hash = " ) ) ;
252+ // Crucially, `All` does NOT emit the match-NONE predicate that empty
253+ // `These` does — the two are opposites.
254+ assert ! ( !stmt. sql. contains( "1=0" ) ) ;
255+ // Only the two fixed params (chain id, raindex) remain.
256+ assert_eq ! ( stmt. params. len( ) , 2 ) ;
257+ assert_eq ! ( stmt. params[ 0 ] , SqlValue :: U64 ( 1 ) ) ;
258+ assert_eq ! ( stmt. params[ 1 ] , SqlValue :: Text ( Address :: ZERO . to_string( ) ) ) ;
259+ }
260+
261+ #[ test]
262+ fn these_empty_emits_match_none_predicate_not_dropped_clause ( ) {
263+ // An empty `These` means *none*: it emits the constant-false predicate
264+ // `AND 1=0` so zero rows match. This is the deliberate opposite of `All`
265+ // (which would drop the clause and return every order). It must NOT
266+ // degenerate to "all".
197267 let stmt = build_fetch_order_trades_batch_stmt (
198268 & RaindexIdentifier :: new ( 1 , Address :: ZERO ) ,
199- & [ ] ,
269+ OrderHashFilter :: These ( & [ ] ) ,
200270 None ,
201271 None ,
202272 )
203273 . unwrap ( ) ;
204274
205- // With no hashes the order-hash WHERE predicate is removed entirely
206- // (the SELECT list still projects tws.order_hash, so only the predicate
207- // forms are asserted absent); only the two fixed params (chain id,
208- // raindex) remain.
275+ // The match-NONE predicate is present...
276+ assert ! ( stmt. sql. contains( "1=0" ) ) ;
277+ // ...and the marker is consumed (not left unsubstituted).
209278 assert ! ( !stmt. sql. contains( ORDER_HASH_CLAUSE ) ) ;
279+ // No IN list / equality predicate and no bound hashes: empty These binds
280+ // zero placeholders.
210281 assert ! ( !stmt. sql. contains( "tws.order_hash IN (" ) ) ;
211282 assert ! ( !stmt. sql. contains( "tws.order_hash = " ) ) ;
283+ // Only the two fixed params (chain id, raindex) — the constant-false
284+ // predicate binds nothing.
212285 assert_eq ! ( stmt. params. len( ) , 2 ) ;
213286 assert_eq ! ( stmt. params[ 0 ] , SqlValue :: U64 ( 1 ) ) ;
214287 assert_eq ! ( stmt. params[ 1 ] , SqlValue :: Text ( Address :: ZERO . to_string( ) ) ) ;
0 commit comments