@@ -15,20 +15,21 @@ use crate::db::datastore::RunnableQuery;
1515use crate :: db:: model;
1616use crate :: db:: model:: SqlU32 ;
1717use crate :: db:: pagination:: paginated;
18+ use crate :: db:: raw_query_builder:: QueryBuilder ;
19+ use crate :: db:: raw_query_builder:: TypedSqlQuery ;
1820use async_bb8_diesel:: AsyncRunQueryDsl ;
1921use diesel:: pg:: Pg ;
2022use diesel:: prelude:: * ;
2123use diesel:: query_builder:: AstPass ;
22- use diesel:: query_builder:: Query ;
2324use diesel:: query_builder:: QueryFragment ;
2425use diesel:: query_builder:: QueryId ;
2526use diesel:: result:: DatabaseErrorKind ;
2627use diesel:: result:: Error as DieselError ;
2728use diesel:: sql_types;
29+ use dropshot:: PaginationOrder ;
2830use nexus_db_errors:: ErrorHandler ;
2931use nexus_db_errors:: public_error_from_diesel;
3032use nexus_db_lookup:: DbConnection ;
31- use nexus_db_schema:: schema;
3233use nexus_db_schema:: schema:: fm_sitrep:: dsl as sitrep_dsl;
3334use nexus_db_schema:: schema:: fm_sitrep_history:: dsl as history_dsl;
3435use nexus_types:: fm;
@@ -224,7 +225,7 @@ impl DataStore {
224225 // TODO(eliza): there should probably be an authz object for the fm sitrep?
225226 opctx. authorize ( authz:: Action :: Modify , & authz:: FLEET ) . await ?;
226227
227- let list = ListOrphanedQuery :: new ( & pagparams)
228+ let list = Self :: list_orphaned_query ( & pagparams)
228229 . load_async :: < Uuid > ( & * conn)
229230 . await
230231 . map_err ( |e| public_error_from_diesel ( e, ErrorHandler :: Server ) ) ?
@@ -234,6 +235,71 @@ impl DataStore {
234235 Ok ( list)
235236 }
236237
238+ fn list_orphaned_query (
239+ pagparams : & DataPageParams < ' _ , SitrepUuid > ,
240+ ) -> TypedSqlQuery < sql_types:: Uuid > {
241+ let mut builder = QueryBuilder :: new ( ) ;
242+ builder. sql (
243+ "WITH current_sitrep_id AS ( \
244+ SELECT sitrep_id \
245+ FROM omicron.public.fm_sitrep_history \
246+ ORDER BY version DESC \
247+ LIMIT 1 \
248+ ),",
249+ ) ;
250+
251+ // batch AS (
252+ // SELECT s.id, s.parent_sitrep_id
253+ // FROM omicron.public.fm_sitrep s
254+ // WHERE s.id > $1
255+ // ORDER BY s.id
256+ // LIMIT $2
257+ // )
258+ let ( dir, cmp) = match pagparams. direction {
259+ PaginationOrder :: Ascending => ( "ASC" , "> " ) ,
260+ PaginationOrder :: Descending => ( "DESC" , "< " ) ,
261+ } ;
262+ builder. sql (
263+ "batch AS ( \
264+ SELECT s.id, s.parent_sitrep_id \
265+ FROM omicron.public.fm_sitrep s ",
266+ ) ;
267+ if let Some ( marker) = pagparams. marker {
268+ builder
269+ . sql ( "WHERE s.id " )
270+ . sql ( cmp)
271+ . param ( )
272+ . bind :: < sql_types:: Uuid , _ > ( marker. into_untyped_uuid ( ) ) ;
273+ }
274+ builder
275+ . sql ( " ORDER BY s.id " )
276+ . sql ( dir)
277+ . sql ( " LIMIT " )
278+ . param ( )
279+ . bind :: < sql_types:: Int8 , _ > ( pagparams. limit . get ( ) as i64 )
280+ . sql ( ") " ) ;
281+
282+ builder. sql (
283+ "SELECT id \
284+ FROM omicron.public.fm_sitrep \
285+ WHERE id IN ( \
286+ SELECT b.id \
287+ FROM batch b \
288+ LEFT JOIN omicron.public.fm_sitrep_history h \
289+ ON h.sitrep_id = b.id \
290+ WHERE
291+ h.sitrep_id IS NULL \
292+ AND (
293+ b.parent_sitrep_id IS NULL \
294+ OR b.parent_sitrep_id != ( \
295+ SELECT sitrep_id FROM current_sitrep_id \
296+ ) \
297+ ) \
298+ );",
299+ ) ;
300+ builder. query ( )
301+ }
302+
237303 /// Deletes all sitreps with the provided IDs.
238304 pub async fn fm_sitrep_delete_all (
239305 & self ,
@@ -469,19 +535,21 @@ impl QueryId for InsertSitrepVersionQuery {
469535 type QueryId = ( ) ;
470536 const HAS_STATIC_QUERY_ID : bool = false ;
471537}
472-
473- type FromClause < T > =
474- diesel:: internal:: table_macro:: StaticQueryFragmentInstance < T > ;
475- const SITREP_FROM_CLAUSE : FromClause < schema:: fm_sitrep:: table > =
476- FromClause :: new ( ) ;
477- const SITREP_HISTORY_FROM_CLAUSE : FromClause < schema:: fm_sitrep_history:: table > =
478- FromClause :: new ( ) ;
479-
480538impl QueryFragment < Pg > for InsertSitrepVersionQuery {
481539 fn walk_ast < ' a > (
482540 & ' a self ,
483541 mut out : AstPass < ' _ , ' a , Pg > ,
484542 ) -> diesel:: QueryResult < ( ) > {
543+ use nexus_db_schema:: schema;
544+
545+ type FromClause < T > =
546+ diesel:: internal:: table_macro:: StaticQueryFragmentInstance < T > ;
547+ const SITREP_FROM_CLAUSE : FromClause < schema:: fm_sitrep:: table > =
548+ FromClause :: new ( ) ;
549+ const SITREP_HISTORY_FROM_CLAUSE : FromClause <
550+ schema:: fm_sitrep_history:: table ,
551+ > = FromClause :: new ( ) ;
552+
485553 const CURRENT_SITREP : & ' static str = "current_sitrep" ;
486554
487555 out. push_sql ( "WITH " ) ;
@@ -605,150 +673,13 @@ impl QueryFragment<Pg> for InsertSitrepVersionQuery {
605673
606674impl RunQueryDsl < DbConnection > for InsertSitrepVersionQuery { }
607675
608- /// CTE for listing orphaned sitreps (paginated).
609- #[ derive( Debug , Clone , Copy ) ]
610- struct ListOrphanedQuery {
611- marker : Option < SitrepUuid > ,
612- order : dropshot:: PaginationOrder ,
613- limit : i32 ,
614- }
615-
616- impl ListOrphanedQuery {
617- fn new ( pagparams : & DataPageParams < ' _ , SitrepUuid > ) -> Self {
618- Self {
619- marker : pagparams. marker . copied ( ) ,
620- order : pagparams. direction ,
621- limit : pagparams. limit . get ( ) as i32 ,
622- }
623- }
624- }
625-
626- impl QueryFragment < Pg > for ListOrphanedQuery {
627- fn walk_ast < ' a > (
628- & ' a self ,
629- mut out : AstPass < ' _ , ' a , Pg > ,
630- ) -> diesel:: QueryResult < ( ) > {
631- // WITH current_sitrep_id AS (
632- // SELECT sitrep_id
633- // FROM omicron.public.fm_sitrep_history
634- // ORDER BY version DESC
635- // LIMIT 1
636- // ),
637- const CURRENT_SITREP_ID : & ' static str = "current_sitrep_id" ;
638- out. push_sql ( "WITH " ) ;
639- out. push_identifier ( CURRENT_SITREP_ID ) ?;
640- out. push_sql ( " AS (SELECT " ) ;
641- out. push_identifier ( history_dsl:: sitrep_id:: NAME ) ?;
642- out. push_sql ( " FROM " ) ;
643- SITREP_HISTORY_FROM_CLAUSE . walk_ast ( out. reborrow ( ) ) ?;
644- out. push_sql ( " ORDER BY " ) ;
645- out. push_identifier ( history_dsl:: version:: NAME ) ?;
646- out. push_sql ( " DESC LIMIT 1)," ) ;
647-
648- // batch AS (
649- // SELECT s.id, s.parent_sitrep_id
650- // FROM omicron.public.fm_sitrep s
651- // WHERE s.id > $1
652- // ORDER BY s.id
653- // LIMIT $2
654- // )
655- const BATCH : & ' static str = "batch" ;
656- out. push_identifier ( BATCH ) ?;
657- out. push_sql ( " AS (SELECT " ) ;
658- out. push_identifier ( sitrep_dsl:: id:: NAME ) ?;
659- out. push_sql ( ", " ) ;
660- out. push_identifier ( sitrep_dsl:: parent_sitrep_id:: NAME ) ?;
661- out. push_sql ( " FROM " ) ;
662- SITREP_FROM_CLAUSE . walk_ast ( out. reborrow ( ) ) ?;
663- if let Some ( ref marker) = self . marker {
664- out. push_sql ( " WHERE " ) ;
665- out. push_identifier ( sitrep_dsl:: id:: NAME ) ?;
666- out. push_sql ( match self . order {
667- dropshot:: PaginationOrder :: Ascending => " > " ,
668- dropshot:: PaginationOrder :: Descending => " < " ,
669- } ) ;
670- out. push_bind_param :: < sql_types:: Uuid , Uuid > (
671- marker. as_untyped_uuid ( ) ,
672- ) ?;
673- }
674- out. push_sql ( " ORDER BY " ) ;
675- out. push_identifier ( sitrep_dsl:: id:: NAME ) ?;
676- match self . order {
677- dropshot:: PaginationOrder :: Ascending => out. push_sql ( " ASC" ) ,
678- dropshot:: PaginationOrder :: Descending => out. push_sql ( " DESC" ) ,
679- }
680- out. push_sql ( " LIMIT " ) ;
681- out. push_bind_param :: < sql_types:: Integer , i32 > ( & self . limit ) ?;
682- out. push_sql ( ") " ) ;
683-
684- // SELECT id
685- // FROM omicron.public.fm_sitrep
686- // WHERE id IN (
687- // SELECT b.id
688- // FROM batch b
689- // -- Lookup the sitrep in the history
690- // LEFT JOIN omicron.public.fm_sitrep_history h ON h.sitrep_id = b.id
691- // -- Find sitreps missing from history
692- // WHERE h.sitrep_id IS NULL
693- // -- ... where the sitrep cannot be made active
694- // AND (b.parent_sitrep_id IS NULL
695- // OR b.parent_sitrep_id != (SELECT sitrep_id FROM current_sitrep_id))
696- // );
697- out. push_sql ( "SELECT " ) ;
698- out. push_identifier ( sitrep_dsl:: id:: NAME ) ?;
699- out. push_sql ( " FROM " ) ;
700- SITREP_FROM_CLAUSE . walk_ast ( out. reborrow ( ) ) ?;
701- out. push_sql ( " WHERE " ) ;
702- out. push_identifier ( sitrep_dsl:: id:: NAME ) ?;
703- out. push_sql ( " IN (" ) ;
704- out. push_sql ( "SELECT " ) ;
705- out. push_identifier ( BATCH ) ?;
706- out. push_sql ( ".id" ) ;
707- out. push_sql ( " FROM " ) ;
708- out. push_identifier ( BATCH ) ?;
709- out. push_sql ( " LEFT JOIN " ) ;
710- SITREP_HISTORY_FROM_CLAUSE . walk_ast ( out. reborrow ( ) ) ?;
711- out. push_sql ( " h ON h." ) ;
712- out. push_identifier ( history_dsl:: sitrep_id:: NAME ) ?;
713- out. push_sql ( " = " ) ;
714- out. push_identifier ( BATCH ) ?;
715- out. push_sql ( "." ) ;
716- out. push_identifier ( sitrep_dsl:: id:: NAME ) ?;
717- out. push_sql ( " WHERE h." ) ;
718- out. push_identifier ( history_dsl:: sitrep_id:: NAME ) ?;
719- out. push_sql ( " IS NULL AND (" ) ;
720- out. push_identifier ( BATCH ) ?;
721- out. push_sql ( "." ) ;
722- out. push_identifier ( sitrep_dsl:: parent_sitrep_id:: NAME ) ?;
723- out. push_sql ( " IS NULL OR " ) ;
724- out. push_identifier ( BATCH ) ?;
725- out. push_sql ( "." ) ;
726- out. push_identifier ( sitrep_dsl:: parent_sitrep_id:: NAME ) ?;
727- out. push_sql ( " != (SELECT sitrep_id FROM " ) ;
728- out. push_identifier ( CURRENT_SITREP_ID ) ?;
729- out. push_sql ( ")));" ) ;
730-
731- Ok ( ( ) )
732- }
733- }
734-
735- impl QueryId for ListOrphanedQuery {
736- type QueryId = ( ) ;
737- const HAS_STATIC_QUERY_ID : bool = false ;
738- }
739-
740- impl RunQueryDsl < DbConnection > for ListOrphanedQuery { }
741-
742- impl Query for ListOrphanedQuery {
743- type SqlType = sql_types:: Uuid ;
744- }
745-
746676#[ cfg( test) ]
747677mod tests {
748678 use super :: * ;
749679 use crate :: db:: explain:: ExplainableAsync ;
750680 use crate :: db:: pagination:: Paginator ;
751681 use crate :: db:: pub_test_utils:: TestDatabase ;
682+ use crate :: db:: raw_query_builder:: expectorate_query_contents;
752683 use chrono:: Utc ;
753684 use nexus_types:: fm;
754685 use omicron_test_utils:: dev;
@@ -788,6 +719,37 @@ mod tests {
788719 logctx. cleanup_successful ( ) ;
789720 }
790721
722+ // Ensure we have the right query contents.
723+ #[ tokio:: test]
724+ async fn expectorate_sitrep_list_orphans_no_marker ( ) {
725+ let pagparams = DataPageParams {
726+ marker : None ,
727+ limit : std:: num:: NonZeroU32 :: new ( 420 ) . unwrap ( ) ,
728+ direction : dropshot:: PaginationOrder :: Descending ,
729+ } ;
730+ let query = DataStore :: list_orphaned_query ( & pagparams) ;
731+ expectorate_query_contents (
732+ & query,
733+ "tests/output/sitrep_list_orphans_no_marker.sql" ,
734+ )
735+ . await ;
736+ }
737+
738+ #[ tokio:: test]
739+ async fn expectorate_sitrep_list_orphans_with_marker ( ) {
740+ let pagparams = DataPageParams {
741+ marker : Some ( & SitrepUuid :: nil ( ) ) ,
742+ limit : std:: num:: NonZeroU32 :: new ( 420 ) . unwrap ( ) ,
743+ direction : dropshot:: PaginationOrder :: Descending ,
744+ } ;
745+ let query = DataStore :: list_orphaned_query ( & pagparams) ;
746+ expectorate_query_contents (
747+ & query,
748+ "tests/output/sitrep_list_orphans_with_marker.sql" ,
749+ )
750+ . await ;
751+ }
752+
791753 #[ tokio:: test]
792754 async fn explain_sitrep_list_orphaned_query ( ) {
793755 let logctx = dev:: test_setup_log ( "explain_sitrep_list_orphaned_query" ) ;
@@ -800,16 +762,7 @@ mod tests {
800762 limit : std:: num:: NonZeroU32 :: new ( 420 ) . unwrap ( ) ,
801763 direction : dropshot:: PaginationOrder :: Descending ,
802764 } ;
803- let query = ListOrphanedQuery :: new ( & pagparams) ;
804-
805- // Before trying to explain the query, elt's start by making sure it's
806- // valid SQL...
807- let q = diesel:: debug_query :: < Pg , _ > ( & query) . to_string ( ) ;
808- match dev:: db:: format_sql ( & q) . await {
809- Ok ( q) => eprintln ! ( "query: {q}" ) ,
810- Err ( e) => panic ! ( "query is malformed: {e}\n {q}" ) ,
811- }
812-
765+ let query = DataStore :: list_orphaned_query ( & pagparams) ;
813766 let explanation = query
814767 . explain_async ( & conn)
815768 . await
0 commit comments