88import java .util .ArrayList ;
99import java .util .Arrays ;
1010import java .util .Collection ;
11- import java .util .HashMap ;
1211import java .util .LinkedHashMap ;
1312import java .util .List ;
1413import java .util .Map ;
5554import org .gusdb .wdk .model .question .Question ;
5655import org .gusdb .wdk .model .record .Field ;
5756import org .gusdb .wdk .model .record .PrimaryKeyDefinition ;
58- import org .gusdb .wdk .model .record .PrimaryKeyValue ;
5957import org .gusdb .wdk .model .record .PrimaryKeyIterator ;
58+ import org .gusdb .wdk .model .record .PrimaryKeyValue ;
6059import org .gusdb .wdk .model .record .RecordClass ;
6160import org .gusdb .wdk .model .record .RecordInstance ;
6261import org .gusdb .wdk .model .record .ResultSetPrimaryKeyIterator ;
6362import org .gusdb .wdk .model .record .TableField ;
6463import org .gusdb .wdk .model .record .attribute .AttributeField ;
6564import org .gusdb .wdk .model .record .attribute .ColumnAttributeField ;
65+ import org .gusdb .wdk .model .record .attribute .PkColumnAttributeField ;
6666import org .gusdb .wdk .model .record .attribute .QueryColumnAttributeField ;
6767import org .gusdb .wdk .model .user .StepContainer ;
6868import org .gusdb .wdk .model .user .User ;
@@ -159,8 +159,8 @@ public class AnswerValue implements PartitionKeysProvider {
159159 private int _startIndex = 1 ;
160160 private int _endIndex = UNBOUNDED_END_PAGE_INDEX ;
161161
162- // sorting for this answer
163- private Map <String , Boolean > _sortingMap ;
162+ // sorting for this answer (true = ascending)
163+ private LinkedHashMap <String , Boolean > _sortingMap ;
164164
165165 // values generated and cached from the above
166166 private String _sortedIdSql ;
@@ -179,7 +179,7 @@ public class AnswerValue implements PartitionKeysProvider {
179179 * @param avoidCacheHit
180180 */
181181 public AnswerValue (RunnableObj <AnswerSpec > validAnswerSpec , int startIndex ,
182- int endIndex , Map <String , Boolean > sortingMap , boolean avoidCacheHit ) throws WdkModelException {
182+ int endIndex , LinkedHashMap <String , Boolean > sortingMap , boolean avoidCacheHit ) throws WdkModelException {
183183 _validAnswerSpec = validAnswerSpec ;
184184 _answerSpec = validAnswerSpec .get ();
185185 _question = _answerSpec .getQuestion ().get (); // must be present if answerspec is valid
@@ -188,7 +188,7 @@ public AnswerValue(RunnableObj<AnswerSpec> validAnswerSpec, int startIndex,
188188 _avoidCacheHit = avoidCacheHit ;
189189 _idsQueryInstance = Query .makeQueryInstance (_answerSpec .getQueryInstanceSpec ().getRunnable ().getLeft (), avoidCacheHit , this );
190190 _resultSizeFactory = new ResultSizeFactory (this );
191- _sortingMap = sortingMap == null ? new HashMap <String , Boolean >() : sortingMap ;
191+ _sortingMap = sortingMap == null ? new LinkedHashMap <String , Boolean >() : sortingMap ;
192192 setPageIndex (startIndex , endIndex );
193193 LOG .debug ("AnswerValue created for question: " + _question .getDisplayName ());
194194 }
@@ -508,35 +508,17 @@ public String getFilteredAttributeSql(
508508 final Query attrQuery ,
509509 final boolean sort
510510 ) throws WdkModelException {
511- String wrapped = joinToIds (getAttributeSql (attrQuery ));
512- wrapped = substitutePartitionKeys (wrapped , attrQuery .getName () + "-getFilteredAttributeSql()" );
513- if (!sort )
514- return wrapped ;
515-
516- final var cols = getSortingColumns ()
517- .stream ()
518- .filter (spec -> spec .getItem () instanceof QueryColumnAttributeField )
519- .iterator ();
520-
521- if (!cols .hasNext ())
522- return wrapped ;
523511
524- final var out = new StringBuilder (wrapped ).append ("\n ORDER BY\n inq." );
525-
526- boolean first = true ;
527- while (cols .hasNext ()) {
528- final var spec = cols .next ();
529-
530- if (!first )
531- out .append ("\n , inq." );
512+ // get attribute SQL joined to IDs
513+ String wrapped = joinToIds (getAttributeSql (attrQuery ));
532514
533- out .append (spec .getItemName ())
534- .append (' ' )
535- .append (spec .getDirection ().toString ());
536- first =false ;
537- }
515+ // substitute partition keys
516+ wrapped = substitutePartitionKeys (wrapped , attrQuery .getName () + "-getFilteredAttributeSql()" );
538517
539- return out .toString ();
518+ // if asked to sort, append generated order-by clause
519+ return !sort ? wrapped :
520+ wrapped + getOrderByClause (_question .getRecordClass ().getPrimaryKeyDefinition (),
521+ _sortingMap , _question .getAttributeFieldMap (), Optional .of ("inq." ));
540522 }
541523
542524 public String getAttributeSql (Query attributeQuery ) throws WdkModelException {
@@ -772,11 +754,14 @@ private void prepareSortingSqls(Map<String, String> sqls, Collection<String> ord
772754 LOG .debug ("AnswerValue: prepareSortingSqls(): sorting map: " + _sortingMap ); //e.g.: {primary_key=true}
773755 final String idQueryNameStub = "answer_id_query" ;
774756 queryNames .put (idQueryNameStub , "idq" );
757+
775758 for (String fieldName : _sortingMap .keySet ()) {
776759 AttributeField field = fields .get (fieldName );
777760 if (field == null ) continue ;
778761 boolean ascend = _sortingMap .get (fieldName );
762+
779763 Map <String , ColumnAttributeField > dependents = field .getColumnAttributeFields ();
764+
780765 for (ColumnAttributeField dependent : dependents .values ()) {
781766
782767 // set default values for PK and simple columns
@@ -869,12 +854,76 @@ public Optional<String> getResultMessage() throws WdkModelException {
869854 return _idsQueryInstance .getResultMessage ();
870855 }
871856
872- public Map <String , Boolean > getSortingMap () {
857+ public void setSortByIdAttribute () {
858+ String idAttribute = _question .getRecordClass ().getIdAttributeField ().getName ();
859+ _sortingMap .clear ();
860+ _sortingMap .put (idAttribute , true );
861+ }
862+
863+ /**
864+ * @return the requested column names to sort and a boolean indication sort direction (true = ascending)
865+ */
866+ public LinkedHashMap <String , Boolean > getSortingMap () {
873867 return new LinkedHashMap <>(_sortingMap );
874868 }
875869
876- public List <SortDirectionSpec <AttributeField >> getSortingColumns () {
877- return SortDirectionSpec .convertSorting (_sortingMap , _question .getAttributeFieldMap ());
870+ /**
871+ * Converts the passed sorting columns into those used to perform the actual sort; note
872+ * these columns could be dependent columns (for derived atrributes) and/or may have been
873+ * mapped to proxy sort columns and may not match the column names returned by getSortingMap().
874+ *
875+ * Note 1: some of this logic is repeated in prepareSortingSqls() and a combination of
876+ * RecorStreamFactory::requiresExactlyOneAttrQuery and FileBasedRecordStream::getRequiredColumnAttributeFields.
877+ *
878+ * Note 2: We append primary key columns to the end to ensure consistent paging; this
879+ * means if you want to avoid runtime by not sorting at all (PKs included) if
880+ * _sortingMap is empty, do NOT call this method. It will always return a sort.
881+ * @throws WdkModelException
882+ */
883+ public static String getOrderByClause (PrimaryKeyDefinition pk , Map <String , Boolean > sortingColumns ,
884+ Map <String , AttributeField > attributeFieldMap , Optional <String > tableAlias ) throws WdkModelException {
885+
886+ String aliasStr = tableAlias .map (a -> a .endsWith ("." ) ? a : a + "." ).orElse ("" );
887+
888+ List <SortDirectionSpec <AttributeField >> baseSorts =
889+ SortDirectionSpec .convertSorting (sortingColumns , attributeFieldMap );
890+
891+ Map <String , String > orderClauses = new LinkedHashMap <>();
892+
893+ for (SortDirectionSpec <AttributeField > spec : baseSorts ) {
894+
895+ for (ColumnAttributeField dependedField : spec .getItem ().getColumnAttributeFields ().values ()) {
896+
897+ if (dependedField instanceof QueryColumnAttributeField ) {
898+
899+ QueryColumnAttributeField queryField = (QueryColumnAttributeField )dependedField ;
900+ Column column = queryField .getColumn ();
901+
902+ // use custom sorting column if specified
903+ String columnName = Optional
904+ .ofNullable (column .getSortingColumn ())
905+ .orElse (column .getName ());
906+
907+ orderClauses .putIfAbsent (columnName , column .isIgnoreCase ()
908+ ? "lower(" + aliasStr + columnName + ") " + spec .getDirection ()
909+ : aliasStr + columnName + " " + spec .getDirection ());
910+ }
911+ else if (dependedField instanceof PkColumnAttributeField ) {
912+ orderClauses .putIfAbsent (dependedField .getName (), dependedField .getName () + " ASC" );
913+ }
914+ else {
915+ throw new IllegalStateException ("Unknown subclass of ColumnAttributeField: " + dependedField .getClass ().getName ());
916+ }
917+ }
918+ }
919+
920+ // for true stable sort, add PK cols to the end if they are not already present
921+ for (String pkCol : pk .getColumnRefs ()) {
922+ orderClauses .putIfAbsent (pkCol , pkCol + " ASC" );
923+ }
924+
925+ // join together
926+ return orderClauses .values ().stream ().collect (Collectors .joining (", " , "\n ORDER BY " , "\n " ));
878927 }
879928
880929 /**
@@ -1048,19 +1097,22 @@ public boolean entireResultRequested() {
10481097 }
10491098
10501099 public static String wrapToReturnOnlyPkAndSelectedCols (String sql ,
1051- RecordClass rc , Collection <QueryColumnAttributeField > fields ) {
1100+ Question question , Collection <QueryColumnAttributeField > fields ,
1101+ Map <String ,Boolean > sortingColumns ) throws WdkModelException {
10521102
1103+ PrimaryKeyDefinition pk = question .getRecordClass ().getPrimaryKeyDefinition ();
10531104 List <String > cols = new ListBuilder <String >()
1054- .addAll (Arrays .asList (rc . getPrimaryKeyDefinition () .getColumnRefs ()))
1105+ .addAll (Arrays .asList (pk .getColumnRefs ()))
10551106 .addAll (fields .stream ().map (Field ::getName ).collect (Collectors .toList ()))
10561107 .toList ();
1057-
1108+
10581109 return new StringBuilder ()
10591110 .append ("/* SingleAttributeRecordStream */\n SELECT\n " )
10601111 .append (String .join (",\n " , cols ))
10611112 .append ("\n FROM (\n " )
10621113 .append (sql )
10631114 .append ("\n ) sarsc" )
1115+ .append (getOrderByClause (pk , sortingColumns , question .getAttributeFieldMap (), Optional .of ("sarsc" )))
10641116 .toString ();
10651117 }
10661118
0 commit comments