@@ -54,6 +54,7 @@ const DEFAULT_PICK_TARGET: &str = "main";
5454const DEFAULT_PICK_STRATEGY : & str = "fail" ;
5555const DEFAULT_PICK_TOP_K : i64 = 5 ;
5656const MAX_PICK_TOP_K : i64 = 100 ;
57+ const MAX_PICK_KEYS : usize = 1000 ;
5758
5859fn safety_prefix ( db_name : Option < & str > ) -> String {
5960 match db_name {
@@ -1110,7 +1111,7 @@ pub async fn call(
11101111 err,
11111112 & pick_args. source ,
11121113 & pick_args. target ,
1113- pick_keys. len ( ) ,
1114+ Some ( pick_keys. len ( ) ) ,
11141115 ) ;
11151116 }
11161117
@@ -1133,10 +1134,10 @@ pub async fn call(
11331134 } else {
11341135 String :: new ( )
11351136 } ;
1136- return Ok ( mcp_text ( & format ! (
1137+ Ok ( mcp_text ( & format ! (
11371138 "Picked {applied} change(s) from branch '{}' into '{}' ({skipped} skipped{ignored_suffix})" ,
11381139 pick_args. source, pick_args. target
1139- ) ) ) ;
1140+ ) ) )
11401141 }
11411142 PickSelector :: SnapshotRange {
11421143 from_snapshot,
@@ -1168,12 +1169,12 @@ pub async fn call(
11681169 )
11691170 . await
11701171 {
1171- return map_pick_conflict ( err, & pick_args. source , & pick_args. target , 0 ) ;
1172+ return map_pick_conflict ( err, & pick_args. source , & pick_args. target , None ) ;
11721173 }
1173- return Ok ( mcp_text ( & format ! (
1174+ Ok ( mcp_text ( & format ! (
11741175 "Picked snapshot range '{}'..'{}' from branch '{}' into '{}'." ,
11751176 from_snapshot, to_snapshot, pick_args. source, pick_args. target
1176- ) ) ) ;
1177+ ) ) )
11771178 }
11781179 PickSelector :: Retrieve { query, top_k } => {
11791180 if top_k <= 0 || top_k > MAX_PICK_TOP_K {
@@ -1187,20 +1188,18 @@ pub async fn call(
11871188 & source_table,
11881189 & target_table,
11891190 None ,
1190- Some ( ( top_k * 10 ) . max ( 20 ) ) ,
1191+ None ,
11911192 )
11921193 . await ?;
1193- let selected = select_retrieved_pick_keys (
1194+ let select_ctx = PickSelectionContext {
11941195 svc,
1195- & sql,
1196+ sql : & sql,
11961197 user_id,
1197- & source_table,
1198- & target_table,
1199- & query,
1200- top_k,
1201- candidates,
1202- )
1203- . await ?;
1198+ source_table : & source_table,
1199+ target_table : & target_table,
1200+ } ;
1201+ let selected =
1202+ select_retrieved_pick_keys ( & select_ctx, & query, top_k, candidates) . await ?;
12041203 if selected. is_empty ( ) {
12051204 return Ok ( mcp_text ( & format ! (
12061205 "No pickable changes matched query '{}' in branch '{}'." ,
@@ -1220,7 +1219,7 @@ pub async fn call(
12201219 err,
12211220 & pick_args. source ,
12221221 & pick_args. target ,
1223- selected. len ( ) ,
1222+ Some ( selected. len ( ) ) ,
12241223 ) ;
12251224 }
12261225 let remaining = count_pick_candidates (
@@ -1237,10 +1236,10 @@ pub async fn call(
12371236 } else {
12381237 0
12391238 } ;
1240- return Ok ( mcp_text ( & format ! (
1239+ Ok ( mcp_text ( & format ! (
12411240 "Picked {applied} of top {top_k} retrieved change(s) from branch '{}' into '{}' ({skipped} skipped)" ,
12421241 pick_args. source, pick_args. target
1243- ) ) ) ;
1242+ ) ) )
12441243 }
12451244 }
12461245 }
@@ -1510,9 +1509,22 @@ fn normalize_keys(keys: Vec<String>) -> Result<Vec<String>, MemoriaError> {
15101509 "key_list selector requires at least one non-empty key" . into ( ) ,
15111510 ) ) ;
15121511 }
1512+ if out. len ( ) > MAX_PICK_KEYS {
1513+ return Err ( MemoriaError :: Validation ( format ! (
1514+ "key_list selector supports at most {MAX_PICK_KEYS} unique keys"
1515+ ) ) ) ;
1516+ }
15131517 Ok ( out)
15141518}
15151519
1520+ struct PickSelectionContext < ' a > {
1521+ svc : & ' a Arc < MemoryService > ,
1522+ sql : & ' a Arc < memoria_storage:: SqlMemoryStore > ,
1523+ user_id : & ' a str ,
1524+ source_table : & ' a str ,
1525+ target_table : & ' a str ,
1526+ }
1527+
15161528async fn resolve_branch_table_name (
15171529 sql : & Arc < memoria_storage:: SqlMemoryStore > ,
15181530 user_id : & str ,
@@ -1661,11 +1673,7 @@ fn vec_to_mo(v: &[f32]) -> String {
16611673}
16621674
16631675async fn select_retrieved_pick_keys (
1664- svc : & Arc < MemoryService > ,
1665- sql : & Arc < memoria_storage:: SqlMemoryStore > ,
1666- user_id : & str ,
1667- source_table : & str ,
1668- target_table : & str ,
1676+ ctx : & PickSelectionContext < ' _ > ,
16691677 query : & str ,
16701678 top_k : i64 ,
16711679 candidates : Vec < PickCandidate > ,
@@ -1683,22 +1691,25 @@ async fn select_retrieved_pick_keys(
16831691 let fetch_k = ( top_k * 3 ) . max ( 20 ) ;
16841692 let mut scores: HashMap < String , f64 > = HashMap :: new ( ) ;
16851693 let diff_clause = pickable_diff_clause ( "s" , "t" ) ;
1686- let safe_user_id = sanitize_sql_literal ( user_id) ;
1694+ let safe_user_id = sanitize_sql_literal ( ctx . user_id ) ;
16871695
1688- if let Some ( embedding) = svc. embed ( query) . await ? {
1696+ if let Some ( embedding) = ctx . svc . embed ( query) . await ? {
16891697 let vec_literal = vec_to_mo ( & embedding) ;
16901698 let sql_text = format ! (
16911699 "SELECT s.memory_id, CAST(l2_distance(s.embedding, '{vec_literal}') AS DOUBLE) AS l2_dist \
16921700 FROM {source_table} s \
16931701 LEFT JOIN {target_table} t ON t.memory_id = s.memory_id \
1694- WHERE s.user_id = '{safe_user_id}' AND s.is_active = 1 \
1695- AND s.embedding IS NOT NULL AND vector_dims(s.embedding) > 0 \
1696- AND ({diff_clause}) \
1697- ORDER BY l2_distance(s.embedding, '{vec_literal}') ASC \
1698- LIMIT {fetch_k} by rank with option 'mode=post'"
1702+ WHERE s.user_id = '{safe_user_id}' AND s.is_active = 1 \
1703+ AND s.embedding IS NOT NULL AND vector_dims(s.embedding) > 0 \
1704+ AND ({diff_clause}) \
1705+ ORDER BY l2_distance(s.embedding, '{vec_literal}') ASC \
1706+ LIMIT {fetch_k} by rank with option 'mode=post'"
1707+ ,
1708+ source_table = ctx. source_table,
1709+ target_table = ctx. target_table
16991710 ) ;
17001711 let rows = sqlx:: query ( & sql_text)
1701- . fetch_all ( sql. pool ( ) )
1712+ . fetch_all ( ctx . sql . pool ( ) )
17021713 . await
17031714 . map_err ( db_err) ?;
17041715 for row in rows {
@@ -1715,14 +1726,17 @@ async fn select_retrieved_pick_keys(
17151726 FROM {source_table} s \
17161727 LEFT JOIN {target_table} t ON t.memory_id = s.memory_id \
17171728 WHERE s.user_id = ? AND s.is_active = 1 \
1718- AND MATCH(s.content) AGAINST('{safe_query}' IN BOOLEAN MODE) \
1719- AND ({diff_clause}) \
1720- ORDER BY ft_score DESC LIMIT ?"
1729+ AND MATCH(s.content) AGAINST('{safe_query}' IN BOOLEAN MODE) \
1730+ AND ({diff_clause}) \
1731+ ORDER BY ft_score DESC LIMIT ?"
1732+ ,
1733+ source_table = ctx. source_table,
1734+ target_table = ctx. target_table
17211735 ) ;
17221736 let rows = match sqlx:: query ( & sql_text)
1723- . bind ( user_id)
1737+ . bind ( ctx . user_id )
17241738 . bind ( fetch_k)
1725- . fetch_all ( sql. pool ( ) )
1739+ . fetch_all ( ctx . sql . pool ( ) )
17261740 . await
17271741 {
17281742 Ok ( rows) => rows,
@@ -1789,12 +1803,15 @@ fn map_pick_conflict(
17891803 err : MemoriaError ,
17901804 source : & str ,
17911805 target : & str ,
1792- selected : usize ,
1806+ selected : Option < usize > ,
17931807) -> Result < Value , MemoriaError > {
17941808 let message = err. to_string ( ) ;
17951809 if message. to_lowercase ( ) . contains ( "conflict" ) {
1810+ let selection_detail = selected
1811+ . map ( |count| format ! ( " for {count} selected change(s)" ) )
1812+ . unwrap_or_default ( ) ;
17961813 return Err ( MemoriaError :: Validation ( format ! (
1797- "Conflict: pick from branch '{source}' into '{target}' aborted for {selected} selected change(s) : {message}"
1814+ "Conflict: pick from branch '{source}' into '{target}' aborted{selection_detail} : {message}"
17981815 ) ) ) ;
17991816 }
18001817 Err ( err)
@@ -1806,7 +1823,10 @@ fn mcp_text(text: &str) -> Value {
18061823
18071824#[ cfg( test) ]
18081825mod tests {
1809- use super :: { safety_prefix, snap_internal, validate_identifier, MAX_IDENTIFIER_LEN } ;
1826+ use super :: {
1827+ normalize_keys, safety_prefix, snap_internal, validate_identifier, MAX_IDENTIFIER_LEN ,
1828+ MAX_PICK_KEYS ,
1829+ } ;
18101830
18111831 #[ test]
18121832 fn scoped_snapshot_internal_names_stay_within_matrixone_limit ( ) {
@@ -1825,6 +1845,15 @@ mod tests {
18251845 assert ! ( prefix. len( ) < MAX_IDENTIFIER_LEN , "{prefix}" ) ;
18261846 }
18271847
1848+ #[ test]
1849+ fn pick_key_list_rejects_too_many_keys ( ) {
1850+ let keys = ( 0 ..=MAX_PICK_KEYS )
1851+ . map ( |idx| format ! ( "memory_{idx}" ) )
1852+ . collect :: < Vec < _ > > ( ) ;
1853+ let err = normalize_keys ( keys) . expect_err ( "should reject oversized key lists" ) ;
1854+ assert ! ( err. to_string( ) . contains( "at most" ) , "{err}" ) ;
1855+ }
1856+
18281857 #[ test]
18291858 fn validate_identifier_accepts_safe_names ( ) {
18301859 assert_eq ! ( validate_identifier( "br_valid_123" ) . unwrap( ) , "br_valid_123" ) ;
0 commit comments