diff --git a/docs/actions/QueryAction.adoc b/docs/actions/QueryAction.adoc index 48ab2450..dfff3655 100644 --- a/docs/actions/QueryAction.adoc +++ b/docs/actions/QueryAction.adoc @@ -33,9 +33,6 @@ If the {link-table} has a `POST_QUERY_CUSTOMIZER` defined, then after records ar * `table` - *String, Required* - Name of the table being queried against. * `filter` - *<> object* - Specification for what records should be returned, based on *<>* objects, and how they should be sorted, based on *<>* objects. If a `filter` is not given, then all rows in the table will be returned by the query. -* `skip` - *Integer* - Optional number of records to be skipped at the beginning of the result set. -e.g., for implementing pagination. -* `limit` - *Integer* - Optional maximum number of records to be returned by the query. * `transaction` - *QBackendTransaction object* - Optional transaction object. ** Behavior for this object is backend-dependant. In an RDBMS backend, this object is generally needed if you want your query to see data that may have been modified within the same transaction. @@ -55,6 +52,14 @@ But if running a query to provide data as part of a process, then this can gener * `shouldMaskPassword` - *boolean, default: true* - Controls whether or not fields with `type` = `PASSWORD` should be masked, or if their actual values should be returned. * `queryJoins` - *List of <> objects* - Optional list of tables to be joined with the main table being queried. See QueryJoin below for further details. +* `fieldNamesToInclude` - *Set of String* - Optional set of field names to be included in the records. +** Fields from a queryJoin must be prefixed by the join table's name or alias, and a period. +Field names from the table being queried should not have any sort of prefix. +** A `null` set here (default) means to include all fields from the table and any queryJoins set as select=true. +** An empty set will cause an error, as well any unrecognized field names. +** `QueryAction` will validate the set of field names, and throw an exception if any unrecognized names are given. +** _Note that this is an optional feature, which some backend modules may not implement. +Meaning, they would always return all fields._ ==== QQueryFilter A key component of *<>*, a *QQueryFilter* defines both what records should be included in a query's results (e.g., an SQL `WHERE`), as well as how those results should be sorted (SQL `ORDER BY`). @@ -68,6 +73,9 @@ In general, multiple *orderBys* can be given (depending on backend implementatio ** Each *subFilter* can include its own additional *subFilters*. ** Each *subFilter* can specify a different *booleanOperator*. ** For example, consider the following *QQueryFilter*, that uses two *subFilters*, and a mix of *booleanOperators* +* `skip` - *Integer* - Optional number of records to be skipped at the beginning of the result set. +e.g., for implementing pagination. +* `limit` - *Integer* - Optional maximum number of records to be returned by the query. [source,java] ---- diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index 4661117e..c106d6c7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -26,6 +26,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -50,6 +51,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperat import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; @@ -64,6 +66,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ListingHash; +import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -101,6 +104,8 @@ public class QueryAction throw (new QException("A table named [" + queryInput.getTableName() + "] was not found in the active QInstance")); } + validateFieldNamesToInclude(queryInput); + QBackendMetaData backend = queryInput.getBackend(); postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_QUERY_RECORD.getRole()); this.queryInput = queryInput; @@ -158,6 +163,109 @@ public class QueryAction + /*************************************************************************** + ** if QueryInput contains a set of FieldNamesToInclude, then validate that + ** those are known field names in the table being queried, or a selected + ** queryJoin. + ***************************************************************************/ + static void validateFieldNamesToInclude(QueryInput queryInput) throws QException + { + Set fieldNamesToInclude = queryInput.getFieldNamesToInclude(); + if(fieldNamesToInclude == null) + { + //////////////////////////////// + // null set means select all. // + //////////////////////////////// + return; + } + + if(fieldNamesToInclude.isEmpty()) + { + ///////////////////////////////////// + // empty set, however, is an error // + ///////////////////////////////////// + throw (new QException("An empty set of fieldNamesToInclude was given as queryInput, which is not allowed.")); + } + + List unrecognizedFieldNames = new ArrayList<>(); + Map selectedQueryJoins = null; + for(String fieldName : fieldNamesToInclude) + { + if(fieldName.contains(".")) + { + //////////////////////////////////////////////// + // handle names with dots - fields from joins // + //////////////////////////////////////////////// + String[] parts = fieldName.split("\\."); + if(parts.length != 2) + { + unrecognizedFieldNames.add(fieldName); + } + else + { + String tableOrAlias = parts[0]; + String fieldNamePart = parts[1]; + + //////////////////////////////////////////// + // build map of queryJoins being selected // + //////////////////////////////////////////// + if(selectedQueryJoins == null) + { + selectedQueryJoins = new HashMap<>(); + for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryInput.getQueryJoins())) + { + if(queryJoin.getSelect()) + { + String joinTableOrAlias = queryJoin.getJoinTableOrItsAlias(); + QTableMetaData joinTable = QContext.getQInstance().getTable(queryJoin.getJoinTable()); + if(joinTable != null) + { + selectedQueryJoins.put(joinTableOrAlias, joinTable); + } + } + } + } + + if(!selectedQueryJoins.containsKey(tableOrAlias)) + { + /////////////////////////////////////////// + // unrecognized tableOrAlias is an error // + /////////////////////////////////////////// + unrecognizedFieldNames.add(fieldName); + } + else + { + QTableMetaData joinTable = selectedQueryJoins.get(tableOrAlias); + if(!joinTable.getFields().containsKey(fieldNamePart)) + { + ////////////////////////////////////////////////////////// + // unrecognized field within the join table is an error // + ////////////////////////////////////////////////////////// + unrecognizedFieldNames.add(fieldName); + } + } + } + } + else + { + /////////////////////////////////////////////////////////////////////// + // non-join fields - just ensure field name is in table's fields map // + /////////////////////////////////////////////////////////////////////// + if(!queryInput.getTable().getFields().containsKey(fieldName)) + { + unrecognizedFieldNames.add(fieldName); + } + } + } + + if(!unrecognizedFieldNames.isEmpty()) + { + throw (new QException("QueryInput contained " + unrecognizedFieldNames.size() + " unrecognized field name" + StringUtils.plural(unrecognizedFieldNames) + ": " + StringUtils.join(",", unrecognizedFieldNames))); + } + } + + + /******************************************************************************* ** shorthand way to call for the most common use-case, when you just want the ** records to be returned, and you just want to pass in a table name and filter. diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java index 5a00b1a3..db4947f3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java @@ -66,6 +66,14 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn private List queryJoins = null; private boolean selectDistinct = false; + ///////////////////////////////////////////////////////////////////////////// + // if this set is null, then the default (all fields) should be included // + // if it's an empty set, that should throw an error // + // or if there are any fields in it that aren't valid fields on the table, // + // or in a selected queryJoin. // + ///////////////////////////////////////////////////////////////////////////// + private Set fieldNamesToInclude; + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // if you say you want to includeAssociations, you can limit which ones by passing them in associationNamesToInclude. // // if you leave it null, you get all associations defined on the table. if you pass it as empty, you get none. // @@ -686,4 +694,35 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn return (queryHints.contains(queryHint)); } + + + /******************************************************************************* + ** Getter for fieldNamesToInclude + *******************************************************************************/ + public Set getFieldNamesToInclude() + { + return (this.fieldNamesToInclude); + } + + + + /******************************************************************************* + ** Setter for fieldNamesToInclude + *******************************************************************************/ + public void setFieldNamesToInclude(Set fieldNamesToInclude) + { + this.fieldNamesToInclude = fieldNamesToInclude; + } + + + + /******************************************************************************* + ** Fluent setter for fieldNamesToInclude + *******************************************************************************/ + public QueryInput withFieldNamesToInclude(Set fieldNamesToInclude) + { + this.fieldNamesToInclude = fieldNamesToInclude; + return (this); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java index 1866385f..98b9572e 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/QueryActionTest.java @@ -24,6 +24,8 @@ package com.kingsrook.qqq.backend.core.actions.tables; import java.io.Serializable; import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import com.kingsrook.qqq.backend.core.BaseTest; @@ -38,6 +40,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -54,6 +57,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -541,4 +545,89 @@ class QueryActionTest extends BaseTest assertEquals(1, QueryAction.execute(TestUtils.TABLE_NAME_SHAPE, new QQueryFilter(new QFilterCriteria("name", QCriteriaOperator.EQUALS, "SqUaRe"))).size()); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testValidateFieldNamesToInclude() throws QException + { + ///////////////////////////// + // cases that do not throw // + ///////////////////////////// + QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withFieldNamesToInclude(null)); + + QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withFieldNamesToInclude(Set.of("id"))); + + QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withFieldNamesToInclude(Set.of("id", "firstName", "lastName", "birthDate", "modifyDate", "createDate"))); + + QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_SHAPE).withSelect(true)) + .withFieldNamesToInclude(Set.of("id", "firstName", "lastName", "birthDate", "modifyDate", "createDate"))); + + QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_SHAPE).withSelect(true)) + .withFieldNamesToInclude(Set.of("id", "firstName", TestUtils.TABLE_NAME_SHAPE + ".id", TestUtils.TABLE_NAME_SHAPE + ".noOfSides"))); + + QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_SHAPE).withSelect(true).withAlias("s")) + .withFieldNamesToInclude(Set.of("id", "s.id", "s.noOfSides"))); + + ////////////////////////// + // cases that do throw! // + ////////////////////////// + assertThatThrownBy(() -> QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withFieldNamesToInclude(new HashSet<>()))) + .hasMessageContaining("empty set of fieldNamesToInclude"); + + assertThatThrownBy(() -> QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withFieldNamesToInclude(Set.of("notAField")))) + .hasMessageContaining("1 unrecognized field name: notAField"); + + assertThatThrownBy(() -> QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withFieldNamesToInclude(new LinkedHashSet<>(List.of("notAField", "alsoNot"))))) + .hasMessageContaining("2 unrecognized field names: notAField,alsoNot"); + + assertThatThrownBy(() -> QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withFieldNamesToInclude(new LinkedHashSet<>(List.of("notAField", "alsoNot", "join.not"))))) + .hasMessageContaining("3 unrecognized field names: notAField,alsoNot,join.not"); + + assertThatThrownBy(() -> QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_SHAPE)) // oops, didn't select it! + .withFieldNamesToInclude(new LinkedHashSet<>(List.of("id", "firstName", TestUtils.TABLE_NAME_SHAPE + ".id", TestUtils.TABLE_NAME_SHAPE + ".noOfSides"))))) + .hasMessageContaining("2 unrecognized field names: shape.id,shape.noOfSides"); + + assertThatThrownBy(() -> QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_SHAPE).withSelect(true)) + .withFieldNamesToInclude(Set.of(TestUtils.TABLE_NAME_SHAPE + ".noField")))) + .hasMessageContaining("1 unrecognized field name: shape.noField"); + + assertThatThrownBy(() -> QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_SHAPE).withSelect(true).withAlias("s")) // oops, not using alias + .withFieldNamesToInclude(new LinkedHashSet<>(List.of("id", "firstName", TestUtils.TABLE_NAME_SHAPE + ".id", "s.noOfSides"))))) + .hasMessageContaining("1 unrecognized field name: shape.id"); + + assertThatThrownBy(() -> QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_SHAPE).withSelect(true)) + .withFieldNamesToInclude(new LinkedHashSet<>(List.of("id", "firstName", TestUtils.TABLE_NAME_SHAPE + ".id", "noOfSides"))))) + .hasMessageContaining("1 unrecognized field name: noOfSides"); + + assertThatThrownBy(() -> QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withQueryJoin(new QueryJoin("noJoinTable").withSelect(true)) + .withFieldNamesToInclude(Set.of("noJoinTable.id")))) + .hasMessageContaining("1 unrecognized field name: noJoinTable.id"); + + assertThatThrownBy(() -> QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withFieldNamesToInclude(Set.of("noJoin.id")))) + .hasMessageContaining("1 unrecognized field name: noJoin.id"); + + assertThatThrownBy(() -> QueryAction.validateFieldNamesToInclude(new QueryInput(TestUtils.TABLE_NAME_PERSON) + .withFieldNamesToInclude(Set.of("noDb.noJoin.id")))) + .hasMessageContaining("1 unrecognized field name: noDb.noJoin.id"); + } + } diff --git a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java index 6738f5fc..8fd22d08 100644 --- a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java +++ b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java @@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -176,18 +177,28 @@ public class AbstractMongoDBAction /******************************************************************************* ** Convert a mongodb document to a QRecord. *******************************************************************************/ - protected QRecord documentToRecord(QTableMetaData table, Document document) + protected QRecord documentToRecord(QueryInput queryInput, Document document) { - QRecord record = new QRecord(); + QTableMetaData table = queryInput.getTable(); + QRecord record = new QRecord(); + record.setTableName(table.getName()); + ///////////////////////////////////////////// + // build the set of field names to include // + ///////////////////////////////////////////// + Set fieldNamesToInclude = queryInput.getFieldNamesToInclude(); + List selectedFields = table.getFields().values() + .stream().filter(field -> fieldNamesToInclude == null || fieldNamesToInclude.contains(field.getName())) + .toList(); + ////////////////////////////////////////////////////////////////////////////////////////////// // first iterate over the table's fields, looking for them (at their backend name (path, // // if it has dots) inside the document note that we'll remove values from the document // // as we go - then after this loop, will handle all remaining values as unstructured fields // ////////////////////////////////////////////////////////////////////////////////////////////// Map values = record.getValues(); - for(QFieldMetaData field : table.getFields().values()) + for(QFieldMetaData field : selectedFields) { String fieldName = field.getName(); String fieldBackendName = getFieldBackendName(field); diff --git a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryAction.java b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryAction.java index 6c4cdc30..dc5801ee 100644 --- a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryAction.java +++ b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryAction.java @@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.module.mongodb.model.metadata.MongoDBBackendMet import com.mongodb.client.FindIterable; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.Projections; import org.bson.Document; import org.bson.conversions.Bson; @@ -96,6 +97,15 @@ public class MongoDBQueryAction extends AbstractMongoDBAction implements QueryIn //////////////////////////////////////////////////////////// FindIterable cursor = collection.find(mongoClientContainer.getMongoSession(), searchQuery); + /////////////////////////////////////////////////////////////////////////////////////////////// + // if input specifies a set of field names to include, then add a 'projection' to the cursor // + /////////////////////////////////////////////////////////////////////////////////////////////// + if(queryInput.getFieldNamesToInclude() != null) + { + List backendFieldNames = queryInput.getFieldNamesToInclude().stream().map(f -> getFieldBackendName(table.getField(f))).toList(); + cursor.projection(Projections.include(backendFieldNames)); + } + /////////////////////////////////// // add a sort operator if needed // /////////////////////////////////// @@ -138,7 +148,7 @@ public class MongoDBQueryAction extends AbstractMongoDBAction implements QueryIn actionTimeoutHelper.cancel(); setQueryStatFirstResultTime(); - QRecord record = documentToRecord(table, document); + QRecord record = documentToRecord(queryInput, document); queryOutput.addRecord(record); if(queryInput.getAsyncJobCallback().wasCancelRequested()) diff --git a/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryActionTest.java b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryActionTest.java index 3e51e7c2..7119ba98 100644 --- a/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryActionTest.java +++ b/qqq-backend-module-mongodb/src/test/java/com/kingsrook/qqq/backend/module/mongodb/actions/MongoDBQueryActionTest.java @@ -29,6 +29,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.context.QContext; @@ -54,6 +55,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; /******************************************************************************* @@ -994,4 +997,46 @@ class MongoDBQueryActionTest extends BaseTest .allMatch(r -> r.getValueInteger("storeKey").equals(1)); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldNamesToInclude() throws QException + { + QQueryFilter filter = new QQueryFilter().withCriteria("firstName", QCriteriaOperator.EQUALS, "Darin"); + QueryInput queryInput = new QueryInput(TestUtils.TABLE_NAME_PERSON).withFilter(filter); + + QRecord record = new QueryAction().execute(queryInput.withFieldNamesToInclude(null)).getRecords().get(0); + assertTrue(record.getValues().containsKey("id")); + assertTrue(record.getValues().containsKey("firstName")); + assertTrue(record.getValues().containsKey("createDate")); + ////////////////////////////////////////////////////////////////////////////// + // note, we get an extra "metaData" field (??), which, i guess is expected? // + ////////////////////////////////////////////////////////////////////////////// + assertEquals(QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON).getFields().size() + 1, record.getValues().size()); + + record = new QueryAction().execute(queryInput.withFieldNamesToInclude(Set.of("id", "firstName"))).getRecords().get(0); + assertTrue(record.getValues().containsKey("id")); + assertTrue(record.getValues().containsKey("firstName")); + assertFalse(record.getValues().containsKey("createDate")); + assertEquals(2, record.getValues().size()); + ////////////////////////////////////////////////////////////////////////////////////////////// + // here, we'd have put _id (which mongo always returns) as "id", since caller requested it. // + ////////////////////////////////////////////////////////////////////////////////////////////// + assertFalse(record.getValues().containsKey("_id")); + + record = new QueryAction().execute(queryInput.withFieldNamesToInclude(Set.of("homeTown"))).getRecords().get(0); + assertFalse(record.getValues().containsKey("id")); + assertFalse(record.getValues().containsKey("firstName")); + assertFalse(record.getValues().containsKey("createDate")); + assertTrue(record.getValues().containsKey("homeTown")); + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // here, mongo always gives back _id (but, we won't have re-mapped it to "id", since caller didn't request it), so, do expect 2 fields here // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + assertEquals(2, record.getValues().size()); + assertTrue(record.getValues().containsKey("_id")); + } + } \ No newline at end of file diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java index 5b687cd7..9f72646f 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -33,10 +33,12 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; import com.kingsrook.qqq.backend.core.actions.tables.helpers.ActionTimeoutHelper; +import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; @@ -92,7 +94,8 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf QTableMetaData table = queryInput.getTable(); String tableName = queryInput.getTableName(); - StringBuilder sql = new StringBuilder(makeSelectClause(queryInput)); + Selection selection = makeSelection(queryInput); + StringBuilder sql = new StringBuilder(selection.selectClause()); QQueryFilter filter = clonedOrNewFilter(queryInput.getFilter()); JoinsContext joinsContext = new JoinsContext(queryInput.getInstance(), tableName, queryInput.getQueryJoins(), filter); @@ -133,23 +136,6 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf needToCloseConnection = true; } - //////////////////////////////////////////////////////////////////////////// - // build the list of fields that will be processed in the result-set loop // - //////////////////////////////////////////////////////////////////////////// - List fieldList = new ArrayList<>(table.getFields().values().stream().toList()); - for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryInput.getQueryJoins())) - { - if(queryJoin.getSelect()) - { - QTableMetaData joinTable = queryInput.getInstance().getTable(queryJoin.getJoinTable()); - String tableNameOrAlias = queryJoin.getJoinTableOrItsAlias(); - for(QFieldMetaData joinField : joinTable.getFields().values()) - { - fieldList.add(joinField.clone().withName(tableNameOrAlias + "." + joinField.getName())); - } - } - } - Long mark = System.currentTimeMillis(); try @@ -189,7 +175,7 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf for(int i = 1; i <= metaData.getColumnCount(); i++) { - QFieldMetaData field = fieldList.get(i - 1); + QFieldMetaData field = selection.fields().get(i - 1); if(!queryInput.getShouldFetchHeavyFields() && field.getIsHeavy()) { @@ -284,26 +270,60 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf - /******************************************************************************* - ** - *******************************************************************************/ - private String makeSelectClause(QueryInput queryInput) throws QException + /*************************************************************************** + ** output wrapper for makeSelection method. + ** - selectClause is everything from SELECT up to (but not including) FROM + ** - fields are those being selected, in the same order, and with mutated + ** names for join fields. + ***************************************************************************/ + private record Selection(String selectClause, List fields) { - QInstance instance = queryInput.getInstance(); + + } + + + + /******************************************************************************* + ** For a given queryInput, determine what fields are being selected - returning + ** a record containing the SELECT clause, as well as a List of QFieldMetaData + ** representing those fields - where - note - the names for fields from join + ** tables will be prefixed by the join table nameOrAlias. + *******************************************************************************/ + private Selection makeSelection(QueryInput queryInput) throws QException + { + QInstance instance = QContext.getQInstance(); String tableName = queryInput.getTableName(); List queryJoins = queryInput.getQueryJoins(); QTableMetaData table = instance.getTable(tableName); - boolean requiresDistinct = queryInput.getSelectDistinct() || doesSelectClauseRequireDistinct(table); - String clausePrefix = (requiresDistinct) ? "SELECT DISTINCT " : "SELECT "; + Set fieldNamesToInclude = queryInput.getFieldNamesToInclude(); - List fieldList = new ArrayList<>(table.getFields().values()); + /////////////////////////////////////////////////////////////////////////////////////////////// + // start with the main table's fields, optionally filtered by the set of fieldNamesToInclude // + /////////////////////////////////////////////////////////////////////////////////////////////// + List fieldList = table.getFields().values() + .stream().filter(field -> fieldNamesToInclude == null || fieldNamesToInclude.contains(field.getName())) + .toList(); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // map those field names to columns, joined with ", ". // + // if a field is heavy, and heavy fields aren't being selected, then replace that field name with a LENGTH function // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// String columns = fieldList.stream() .map(field -> Pair.of(field, escapeIdentifier(tableName) + "." + escapeIdentifier(getColumnName(field)))) .map(pair -> wrapHeavyFieldsWithLengthFunctionIfNeeded(pair, queryInput.getShouldFetchHeavyFields())) .collect(Collectors.joining(", ")); - StringBuilder rs = new StringBuilder(clausePrefix).append(columns); + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + // figure out if distinct is being used. then start building the select clause with the table's columns // + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + boolean requiresDistinct = queryInput.getSelectDistinct() || doesSelectClauseRequireDistinct(table); + StringBuilder selectClause = new StringBuilder((requiresDistinct) ? "SELECT DISTINCT " : "SELECT ").append(columns); + List selectionFieldList = new ArrayList<>(fieldList); + + /////////////////////////////////// + // add any 'selected' queryJoins // + /////////////////////////////////// for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins)) { if(queryJoin.getSelect()) @@ -315,16 +335,31 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf throw new QException("Requested join table [" + queryJoin.getJoinTable() + "] is not a defined table."); } - List joinFieldList = new ArrayList<>(joinTable.getFields().values()); + /////////////////////////////////// + // filter by fieldNamesToInclude // + /////////////////////////////////// + List joinFieldList = joinTable.getFields().values() + .stream().filter(field -> fieldNamesToInclude == null || fieldNamesToInclude.contains(tableNameOrAlias + "." + field.getName())) + .toList(); + + ///////////////////////////////////////////////////// + // map to columns, wrapping heavy fields as needed // + ///////////////////////////////////////////////////// String joinColumns = joinFieldList.stream() .map(field -> Pair.of(field, escapeIdentifier(tableNameOrAlias) + "." + escapeIdentifier(getColumnName(field)))) .map(pair -> wrapHeavyFieldsWithLengthFunctionIfNeeded(pair, queryInput.getShouldFetchHeavyFields())) .collect(Collectors.joining(", ")); - rs.append(", ").append(joinColumns); + + //////////////////////////////////////////////////////////////////////////////////////////////// + // append to output objects. // + // note that fields are cloned, since we are changing their names to have table/alias prefix. // + //////////////////////////////////////////////////////////////////////////////////////////////// + selectClause.append(", ").append(joinColumns); + selectionFieldList.addAll(joinFieldList.stream().map(field -> field.clone().withName(tableNameOrAlias + "." + field.getName())).toList()); } } - return (rs.toString()); + return (new Selection(selectClause.toString(), selectionFieldList)); } diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionJoinsTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionJoinsTest.java index 44dfbad0..867b4726 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionJoinsTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionJoinsTest.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.module.rdbms.actions; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; @@ -1052,4 +1053,30 @@ public class RDBMSQueryActionJoinsTest extends RDBMSActionTest assertEquals(5, new QueryAction().execute(new QueryInput(TestUtils.TABLE_NAME_PERSON)).getRecords().size()); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldNamesToInclude() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_PERSONAL_ID_CARD).withSelect(true)); + queryInput.withFieldNamesToInclude(Set.of("firstName", TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber")); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertThat(queryOutput.getRecords()).allMatch(r -> r.getValues().containsKey("firstName")); + assertThat(queryOutput.getRecords()).allMatch(r -> r.getValues().containsKey(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber")); + assertThat(queryOutput.getRecords()).allMatch(r -> r.getValues().size() == 2); + + //////////////////////////////////////////////////////////////////////////////////////////////////// + // re-run w/ null fieldNamesToInclude -- and should still see those 2, and more (values size > 2) // + //////////////////////////////////////////////////////////////////////////////////////////////////// + queryInput.withFieldNamesToInclude(null); + queryOutput = new QueryAction().execute(queryInput); + assertThat(queryOutput.getRecords()).allMatch(r -> r.getValues().containsKey("firstName")); + assertThat(queryOutput.getRecords()).allMatch(r -> r.getValues().containsKey(TestUtils.TABLE_NAME_PERSONAL_ID_CARD + ".idNumber")); + assertThat(queryOutput.getRecords()).allMatch(r -> r.getValues().size() > 2); + } + } diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java index fa27d4ea..b3bf3975 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java @@ -28,6 +28,7 @@ import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Predicate; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; @@ -48,11 +49,12 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.module.rdbms.TestUtils; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; /******************************************************************************* @@ -166,7 +168,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").equals(email)), "Should NOT find expected email address"); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").equals(email)), "Should NOT find expected email address"); } @@ -199,7 +201,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withValues(List.of(1_000_000)))); queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().noneMatch(r -> Objects.equals(1_000_000, r.getValueInteger("annualSalary"))), "Should NOT find expected salary"); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> Objects.equals(1_000_000, r.getValueInteger("annualSalary"))), "Should NOT find expected salary"); } @@ -219,7 +221,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(2) || r.getValueInteger("id").equals(4)), "Should find expected ids"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(2) || r.getValueInteger("id").equals(4)), "Should find expected ids"); } @@ -239,7 +241,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(5)), "Should find expected ids"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(5)), "Should find expected ids"); } @@ -259,7 +261,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches("darin.*")), "Should find matching email address"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches("darin.*")), "Should find matching email address"); } @@ -279,7 +281,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address"); } @@ -299,7 +301,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address"); } @@ -319,7 +321,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address"); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address"); } @@ -339,7 +341,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches(".*gmail.com")), "Should find matching email address"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueString("email").matches(".*gmail.com")), "Should find matching email address"); } @@ -359,7 +361,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches("darin.*")), "Should find matching email address"); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches("darin.*")), "Should find matching email address"); } @@ -379,7 +381,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address"); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches(".*kelkhoff.*")), "Should find matching email address"); } @@ -399,7 +401,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches(".*gmail.com")), "Should find matching email address"); + assertTrue(queryOutput.getRecords().stream().noneMatch(r -> r.getValueString("email").matches(".*gmail.com")), "Should find matching email address"); } @@ -419,7 +421,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(2)), "Should find expected ids"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(2)), "Should find expected ids"); } @@ -439,7 +441,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(2)), "Should find expected ids"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(2)), "Should find expected ids"); } @@ -459,7 +461,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(4) || r.getValueInteger("id").equals(5)), "Should find expected ids"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(4) || r.getValueInteger("id").equals(5)), "Should find expected ids"); } @@ -479,7 +481,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest ); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(4) || r.getValueInteger("id").equals(5)), "Should find expected ids"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(4) || r.getValueInteger("id").equals(5)), "Should find expected ids"); } @@ -498,7 +500,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest )); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValue("birthDate") == null), "Should find expected row"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValue("birthDate") == null), "Should find expected row"); } @@ -517,7 +519,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest )); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(5, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValue("firstName") != null), "Should find expected rows"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValue("firstName") != null), "Should find expected rows"); } @@ -537,7 +539,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest )); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(3, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(2) || r.getValueInteger("id").equals(3) || r.getValueInteger("id").equals(4)), "Should find expected ids"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(2) || r.getValueInteger("id").equals(3) || r.getValueInteger("id").equals(4)), "Should find expected ids"); } @@ -557,7 +559,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest )); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(5)), "Should find expected ids"); + assertTrue(queryOutput.getRecords().stream().allMatch(r -> r.getValueInteger("id").equals(1) || r.getValueInteger("id").equals(5)), "Should find expected ids"); } @@ -583,7 +585,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.LESS_THAN).withValues(List.of(new Now())))); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row"); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row"); } { @@ -593,7 +595,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.LESS_THAN).withValues(List.of(NowWithOffset.plus(2, ChronoUnit.DAYS))))); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row"); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row"); } { @@ -603,8 +605,8 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.GREATER_THAN).withValues(List.of(NowWithOffset.minus(5, ChronoUnit.DAYS))))); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); - Assertions.assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row"); - Assertions.assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("future")), "Should find expected row"); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("past")), "Should find expected row"); + assertTrue(queryOutput.getRecords().stream().anyMatch(r -> r.getValue("firstName").equals("future")), "Should find expected row"); } } @@ -1004,7 +1006,36 @@ public class RDBMSQueryActionTest extends RDBMSActionTest queryInput.setShouldFetchHeavyFields(true); records = new QueryAction().execute(queryInput).getRecords(); assertThat(records).describedAs("Some records should have the heavy homeTown field set when heavies are requested").anyMatch(r -> r.getValue("homeTown") != null); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldNamesToInclude() throws QException + { + QQueryFilter filter = new QQueryFilter().withCriteria("id", QCriteriaOperator.EQUALS, 1); + QueryInput queryInput = new QueryInput(TestUtils.TABLE_NAME_PERSON).withFilter(filter); + + QRecord record = new QueryAction().execute(queryInput.withFieldNamesToInclude(null)).getRecords().get(0); + assertTrue(record.getValues().containsKey("id")); + assertTrue(record.getValues().containsKey("firstName")); + assertTrue(record.getValues().containsKey("createDate")); + assertEquals(QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON).getFields().size(), record.getValues().size()); + + record = new QueryAction().execute(queryInput.withFieldNamesToInclude(Set.of("id", "firstName"))).getRecords().get(0); + assertTrue(record.getValues().containsKey("id")); + assertTrue(record.getValues().containsKey("firstName")); + assertFalse(record.getValues().containsKey("createDate")); + assertEquals(2, record.getValues().size()); + + record = new QueryAction().execute(queryInput.withFieldNamesToInclude(Set.of("homeTown"))).getRecords().get(0); + assertFalse(record.getValues().containsKey("id")); + assertFalse(record.getValues().containsKey("firstName")); + assertFalse(record.getValues().containsKey("createDate")); + assertEquals(1, record.getValues().size()); } }