@@ -533,7 +533,6 @@ private SearchResponse performSpannerSearch(SearchRequest request,
533533 LOG .info ("Executing Spanner SQL Template: {}" , statement .getSql ());
534534 LOG .info ("With Parameters: {}" , statement .getParameters ());
535535
536- // 3. Execute and process results.
537536 ResultSet resultSet = transaction .executeQuery (statement );
538537 List <MetadataRecord > results = new ArrayList <>();
539538 String nextActualCursor = null ;
@@ -545,8 +544,7 @@ private SearchResponse performSpannerSearch(SearchRequest request,
545544
546545 LOG .info ("Found {} results." , results .size ());
547546
548- // 4. Create the final response, including the next page's cursor.
549- return createSpannerSearchResponse (request , results , nextActualCursor );
547+ return createSearchResponse (request , results , nextActualCursor );
550548
551549 }
552550 }
@@ -563,18 +561,16 @@ private QueryBuildResult buildSpannerQuery(SearchRequest request, @Nullable Curs
563561 List <String > sortColumns = sortDetails .getColumns ();
564562 List <Sorting .Order > sortOrders = sortDetails .getOrders ();
565563
566- // Step 2: Add standard filter conditions (namespaces, types, etc.) to the WHERE clause.
564+ // Add standard filter conditions (namespaces, types, etc.) to the WHERE clause.
567565 appendFilterConditions (request , conditions , params );
568566
569- // Step 3: Add the special WHERE condition for keyset pagination if a cursor exists.
567+ // Add the special WHERE condition for keyset pagination if a cursor exists.
570568 appendCursorCondition (requestCursor , sortColumns , sortOrders , conditions , params );
571-
572- // Step 4: Assemble the final query string.
573569 if (!conditions .isEmpty ()) {
574570 sql .append (" WHERE " ).append (String .join (" AND " , conditions ));
575571 }
576572
577- // Step 5: Add the ORDER BY clause.
573+ // Add the ORDER BY clause.
578574 List <String > orderByClauses = new ArrayList <>();
579575 for (int i = 0 ; i < sortColumns .size (); i ++) {
580576 orderByClauses .add (sortColumns .get (i ) + " " + sortOrders .get (i ).name ());
@@ -613,7 +609,6 @@ private SortDetailsResult getSortDetails(SearchRequest request) {
613609 orders .add (Sorting .Order .ASC );
614610 }
615611
616- // Return the new type-safe object
617612 return new SortDetailsResult (columns , orders );
618613 }
619614
@@ -719,7 +714,6 @@ private String convertToRegexpPattern(String sqlWildcardPattern) {
719714 }
720715
721716 /**
722- * UPDATED HELPER METHOD
723717 * Builds the condition for a simple term search based on the provided scope.
724718 * Now uses the precise SEARCH_SUBSTRING syntax.
725719 *
@@ -751,46 +745,59 @@ private String buildScopedSearchCondition(String searchTerm, @Nullable MetadataS
751745 }
752746
753747 /**
754- * NEW HELPER METHOD
755748 * Appends the complex WHERE condition for keyset pagination if a cursor is provided.
756749 * This isolates the most complicated part of the query construction.
757750 */
758751 private void appendCursorCondition (@ Nullable Cursor requestCursor , List <String > sortColumns ,
759752 List <Sorting .Order > sortOrders , List <String > conditions ,
760753 Map <String , Value > params ) {
761- if (requestCursor == null || requestCursor .getActualCursor () == null || requestCursor .getActualCursor ().isEmpty ()) {
762- return ;
754+ // 1. Validate the cursor
755+ if (requestCursor == null || requestCursor .getActualCursor () == null ) {
756+ return ; // No cursor, no pagination condition needed.
763757 }
764758
765759 String [] cursorValues = requestCursor .getActualCursor ().split ("," , -1 );
766760 if (cursorValues .length != sortColumns .size ()) {
767- LOG .warn ("Cursor values do not match sort columns. Ignoring cursor. Cursor: '{}', SortColumns: '{}'" ,
768- requestCursor .getActualCursor (), sortColumns );
769- return ;
761+ LOG .warn ("Cursor values count ({}) does not match sort columns count ({}). Ignoring cursor. " +
762+ "Cursor: '{}', SortColumns: '{}'" ,
763+ cursorValues .length , sortColumns .size (), requestCursor .getActualCursor (), sortColumns );
764+ return ; // Invalid cursor format, cannot apply pagination.
770765 }
771766
772- List <String > pageConditions = new ArrayList <>();
767+ List <String > combinedRowConditions = new ArrayList <>();
768+ List <String > prefixEqualityConditions = new ArrayList <>();
769+
770+ // Iterate through each column in the sort order to build the multi-column comparison
773771 for (int i = 0 ; i < sortColumns .size (); i ++) {
774- List <String > keyConditions = new ArrayList <>();
775- String colName = sortColumns .get (i );
776-
777- for (int j = 0 ; j < i ; j ++) {
778- String priorCol = sortColumns .get (j );
779- String paramName = "cursor_" + priorCol ;
780- keyConditions .add (String .format ("%s = @%s" , priorCol , paramName ));
781- params .put (paramName , Value .string (cursorValues [j ]));
772+ String currentSortColumn = sortColumns .get (i );
773+ Sorting .Order currentSortOrder = sortOrders .get (i );
774+ String currentCursorValue = cursorValues [i ];
775+
776+ // Add the equality condition for the *previous* column to the prefix list
777+ if (i > 0 ) {
778+ String priorColumn = sortColumns .get (i - 1 );
779+ String priorCursorValue = cursorValues [i - 1 ];
780+ String paramName = "cursor_eq_" + priorColumn + "_" + (i - 1 );
781+ params .put (paramName , Value .string (priorCursorValue ));
782+ prefixEqualityConditions .add (String .format ("%s = @%s" , priorColumn , paramName ));
782783 }
783784
784- String operator = sortOrders .get (i ) == Sorting .Order .ASC ? ">" : "<" ;
785- String paramName = "cursor_" + colName ;
786- keyConditions .add (String .format ("%s %s @%s" , colName , operator , paramName ));
787- params .put (paramName , Value .string (cursorValues [i ]));
785+ // Start building the conditions for the current OR clause
786+ List <String > currentClauseConditions = new ArrayList <>(prefixEqualityConditions );
787+ String operator = (currentSortOrder == Sorting .Order .ASC ) ? ">" : "<" ;
788+ String paramName = "cursor_gtlt_" + currentSortColumn + "_" + i ;
789+ params .put (paramName , Value .string (currentCursorValue ));
790+ currentClauseConditions .add (String .format ("%s %s @%s" , currentSortColumn , operator , paramName ));
791+ combinedRowConditions .add ("(" + String .join (" AND " , currentClauseConditions ) + ")" );
792+ }
788793
789- pageConditions .add ("(" + String .join (" AND " , keyConditions ) + ")" );
794+ // 3. Add the final OR-ed condition to the main conditions list
795+ if (!combinedRowConditions .isEmpty ()) {
796+ conditions .add ("(" + String .join (" OR " , combinedRowConditions ) + ")" );
790797 }
791- conditions .add ("(" + String .join (" OR " , pageConditions ) + ")" );
792798 }
793799
800+
794801 /**
795802 * Creates the "actualCursor" part of the next cursor. This is the "bookmark" itself.
796803 * It's now based on the actual columns that were used for sorting.
@@ -807,7 +814,7 @@ private String createNextCursorKey(ResultSet resultSet, List<String> sortColumns
807814 /**
808815 * Creates the final SearchResponse, correctly packaging the next cursor string.
809816 */
810- private SearchResponse createSpannerSearchResponse (SearchRequest request , List <MetadataRecord > results ,
817+ private SearchResponse createSearchResponse (SearchRequest request , List <MetadataRecord > results ,
811818 String nextActualCursor ) {
812819 String finalCursorString = (nextActualCursor != null ) ?
813820 getCursor (request , results , nextActualCursor ).toString () : null ;
@@ -838,12 +845,9 @@ private Cursor getCursor(SearchRequest request, List<MetadataRecord> results, St
838845 private MetadataRecord mapSpannerResult (ResultSet resultSet ) {
839846 String documentId = resultSet .getString (Tables .Metadata .METADATA_ID_FIELD );
840847 Struct row = resultSet .getCurrentRowAsStruct ();
841- LOG .info (row .toString ());
842848 String metadataJson = row .getJson (7 );
843849 Metadata metadata = GSON .fromJson (metadataJson , Metadata .class );
844850 MetadataEntity entity = toMetadataEntity (documentId );
845-
846-
847851 return new MetadataRecord (entity , metadata );
848852 }
849853
@@ -861,25 +865,25 @@ private static String mapSortKey(String key) {
861865 }
862866
863867 /**
864- * Translate a document id in the index into a metadata entity.
868+ * Translate a metadata id in the index into a metadata entity.
865869 */
866- private static MetadataEntity toMetadataEntity (String documentId ) {
867- int index = documentId .indexOf (':' );
870+ private static MetadataEntity toMetadataEntity (String metadataId ) {
871+ int index = metadataId .indexOf (':' );
868872 if (index < 0 ) {
869873 throw new IllegalArgumentException (
870- "Document Id must be of the form 'type:k=v,...' but is " + documentId );
874+ "Metadata Id must be of the form 'type:k=v,...' but is " + metadataId );
871875 }
872- String type = documentId .substring (0 , index );
876+ String type = metadataId .substring (0 , index );
873877 MetadataEntity .Builder builder = MetadataEntity .builder ();
874- for (String part : documentId .substring (index + 1 ).split ("," )) {
878+ for (String part : metadataId .substring (index + 1 ).split ("," )) {
875879 String [] parts = part .split ("=" , 2 );
876880 if (parts [0 ].equals (type )) {
877881 builder .appendAsType (parts [0 ], parts [1 ]);
878882 } else {
879883 builder .append (parts [0 ], parts [1 ]);
880884 }
881885 }
882- // TODO (CDAP-13597): Handle versioning of metadata entities in a better way
886+
883887 // if it is a versioned entity then add the default version
884888 return MetadataUtil .addVersionIfNeeded (builder .build ());
885889 }
0 commit comments