@@ -7,7 +7,10 @@ use entity::{
7
7
use regex:: Regex ;
8
8
use sea_orm:: {
9
9
prelude:: Expr ,
10
- sea_query:: { Alias , ColumnRef , CommonTableExpression , IntoIden , OnConflict , Query , WithClause } ,
10
+ sea_query:: {
11
+ Alias , ColumnRef , CommonTableExpression , IntoIden , OnConflict , Query , WindowStatement ,
12
+ WithClause ,
13
+ } ,
11
14
ActiveValue :: NotSet ,
12
15
ColumnTrait , ConnectionTrait , DbErr , EntityTrait , FromQueryResult , IntoSimpleExpr , Iterable ,
13
16
Order , QuerySelect ,
@@ -46,50 +49,147 @@ where
46
49
Ok ( ( ) )
47
50
}
48
51
49
- pub async fn list < C > (
50
- db : & C ,
51
- address : Option < AddressAlloy > ,
52
- query : Option < String > ,
53
- chain_id : Option < ChainId > ,
54
- token_types : Option < Vec < db_enum:: TokenType > > ,
55
- page_size : u64 ,
56
- page_token : Option < ( AddressAlloy , ChainId ) > ,
57
- ) -> Result < ( Vec < Model > , Option < ( AddressAlloy , ChainId ) > ) , DbErr >
58
- where
59
- C : ConnectionTrait ,
60
- {
52
+ fn prepare_address_search_cte (
53
+ contract_name_query : Option < String > ,
54
+ cte_name : impl IntoIden ,
55
+ ) -> CommonTableExpression {
61
56
// Materialize addresses CTE when searching by contract_name.
62
57
// Otherwise, query planner chooses a suboptimal plan.
63
- let is_cte_materialized = query. is_some ( ) ;
64
- let addresses_cte_iden = Alias :: new ( "addresses" ) . into_iden ( ) ;
65
- let addresses_cte = CommonTableExpression :: new ( )
58
+ // If query is not provided, this CTE will be folded by the optimizer.
59
+ let is_cte_materialized = contract_name_query. is_some ( ) ;
60
+
61
+ CommonTableExpression :: new ( )
66
62
. query (
67
63
QuerySelect :: query ( & mut Entity :: find ( ) )
68
- . apply_if ( query , |q, query| {
64
+ . apply_if ( contract_name_query , |q, query| {
69
65
let ts_query = prepare_ts_query ( & query) ;
70
66
q. and_where ( Expr :: cust_with_expr (
71
67
"to_tsvector('english', contract_name) @@ to_tsquery($1)" ,
72
68
ts_query,
73
69
) ) ;
74
70
} )
71
+ // Apply a hard limit in case we materialize the CTE
72
+ . apply_if ( is_cte_materialized. then_some ( 10_000 ) , |q, limit| {
73
+ q. limit ( limit) ;
74
+ } )
75
75
. to_owned ( ) ,
76
76
)
77
77
. materialized ( is_cte_materialized)
78
- . table_name ( addresses_cte_iden. clone ( ) )
78
+ . table_name ( cte_name)
79
+ . to_owned ( )
80
+ }
81
+
82
+ pub async fn uniform_chain_search < C > (
83
+ db : & C ,
84
+ contract_name_query : String ,
85
+ token_types : Option < Vec < db_enum:: TokenType > > ,
86
+ chain_ids : Vec < ChainId > ,
87
+ ) -> Result < Vec < Model > , DbErr >
88
+ where
89
+ C : ConnectionTrait ,
90
+ {
91
+ if chain_ids. is_empty ( ) {
92
+ return Ok ( vec ! [ ] ) ;
93
+ }
94
+
95
+ let ts_rank_ordering = Expr :: cust_with_expr (
96
+ "ts_rank(to_tsvector('english', contract_name), to_tsquery($1))" ,
97
+ prepare_ts_query ( & contract_name_query) ,
98
+ ) ;
99
+
100
+ let addresses_cte_iden = Alias :: new ( "addresses" ) . into_iden ( ) ;
101
+ let addresses_cte =
102
+ prepare_address_search_cte ( Some ( contract_name_query) , addresses_cte_iden. clone ( ) ) ;
103
+
104
+ let row_number = Expr :: custom_keyword ( Alias :: new ( "ROW_NUMBER()" ) ) ;
105
+ let ranked_addresses_iden = Alias :: new ( "ranked_addresses" ) . into_iden ( ) ;
106
+ let ranked_addresses_cte = CommonTableExpression :: new ( )
107
+ . query (
108
+ Query :: select ( )
109
+ . column ( ColumnRef :: TableAsterisk ( addresses_cte_iden. clone ( ) ) )
110
+ . expr_window_as (
111
+ row_number,
112
+ WindowStatement :: partition_by ( Column :: ChainId )
113
+ . order_by_expr ( ts_rank_ordering, Order :: Desc )
114
+ . order_by ( Column :: Hash , Order :: Asc )
115
+ . to_owned ( ) ,
116
+ Alias :: new ( "rn" ) ,
117
+ )
118
+ . from ( addresses_cte_iden. clone ( ) )
119
+ . and_where ( Column :: ChainId . is_in ( chain_ids. clone ( ) ) )
120
+ . apply_if ( token_types, |q, token_types| {
121
+ if !token_types. is_empty ( ) {
122
+ q. and_where ( Column :: TokenType . is_in ( token_types) ) ;
123
+ } else {
124
+ q. and_where ( Column :: TokenType . is_null ( ) ) ;
125
+ }
126
+ } )
127
+ . to_owned ( ) ,
128
+ )
129
+ . table_name ( ranked_addresses_iden. clone ( ) )
130
+ . to_owned ( ) ;
131
+
132
+ let limit = chain_ids. len ( ) as u64 ;
133
+ let base_select = Query :: select ( )
134
+ . column ( ColumnRef :: Asterisk )
135
+ . from ( ranked_addresses_iden)
136
+ . and_where ( Expr :: col ( Alias :: new ( "rn" ) ) . eq ( 1 ) )
137
+ . order_by_expr (
138
+ Expr :: cust_with_exprs (
139
+ "array_position($1, $2)" ,
140
+ [ chain_ids. into ( ) , Expr :: col ( Column :: ChainId ) . into ( ) ] ,
141
+ ) ,
142
+ Order :: Asc ,
143
+ )
144
+ . limit ( limit)
79
145
. to_owned ( ) ;
80
146
147
+ let query = WithClause :: new ( )
148
+ . cte ( addresses_cte)
149
+ . cte ( ranked_addresses_cte)
150
+ . to_owned ( )
151
+ . query ( base_select) ;
152
+
153
+ let addresses = Model :: find_by_statement ( db. get_database_backend ( ) . build ( & query) )
154
+ . all ( db)
155
+ . await ?;
156
+
157
+ Ok ( addresses)
158
+ }
159
+
160
+ pub async fn list < C > (
161
+ db : & C ,
162
+ address : Option < AddressAlloy > ,
163
+ contract_name_query : Option < String > ,
164
+ chain_ids : Option < Vec < ChainId > > ,
165
+ token_types : Option < Vec < db_enum:: TokenType > > ,
166
+ page_size : u64 ,
167
+ page_token : Option < ( AddressAlloy , ChainId ) > ,
168
+ ) -> Result < ( Vec < Model > , Option < ( AddressAlloy , ChainId ) > ) , DbErr >
169
+ where
170
+ C : ConnectionTrait ,
171
+ {
172
+ let addresses_cte_iden = Alias :: new ( "addresses" ) . into_iden ( ) ;
173
+ let addresses_cte = prepare_address_search_cte ( contract_name_query, addresses_cte_iden. clone ( ) ) ;
174
+
81
175
let base_select = Query :: select ( )
82
176
. column ( ColumnRef :: Asterisk )
83
177
. from ( addresses_cte_iden)
84
- . apply_if ( chain_id, |q, chain_id| {
85
- q. and_where ( Column :: ChainId . eq ( chain_id) ) ;
178
+ . apply_if ( chain_ids, |q, chain_ids| {
179
+ if !chain_ids. is_empty ( ) {
180
+ q. and_where ( Column :: ChainId . is_in ( chain_ids) ) ;
181
+ }
182
+ } )
183
+ . apply_if ( token_types, |q, token_types| {
184
+ if !token_types. is_empty ( ) {
185
+ q. and_where ( Column :: TokenType . is_in ( token_types) ) ;
186
+ } else {
187
+ q. and_where ( Column :: TokenType . is_null ( ) ) ;
188
+ }
86
189
} )
87
190
. apply_if ( address, |q, address| {
88
191
q. and_where ( Column :: Hash . eq ( address. as_slice ( ) ) ) ;
89
192
} )
90
- . apply_if ( token_types, |q, token_types| {
91
- q. and_where ( Column :: TokenType . is_in ( token_types) ) ;
92
- } )
93
193
. apply_if ( page_token, |q, page_token| {
94
194
q. and_where (
95
195
Expr :: tuple ( [
0 commit comments