CE-1405 / CE-1479 - add queryInput.fieldNamesToInclude

This commit is contained in:
2024-08-15 08:53:19 -05:00
parent 51eb7d89be
commit 9a65ea81b2
10 changed files with 466 additions and 63 deletions

View File

@ -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. * `table` - *String, Required* - Name of the table being queried against.
* `filter` - *<<QQueryFilter>> object* - Specification for what records should be returned, based on *<<QFilterCriteria>>* objects, and how they should be sorted, based on *<<QFilterOrderBy>>* objects. * `filter` - *<<QQueryFilter>> object* - Specification for what records should be returned, based on *<<QFilterCriteria>>* objects, and how they should be sorted, based on *<<QFilterOrderBy>>* objects.
If a `filter` is not given, then all rows in the table will be returned by the query. 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. * `transaction` - *QBackendTransaction object* - Optional transaction object.
** Behavior for this object is backend-dependant. ** 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. 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. * `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 <<QueryJoin>> objects* - Optional list of tables to be joined with the main table being queried. * `queryJoins` - *List of <<QueryJoin>> objects* - Optional list of tables to be joined with the main table being queried.
See QueryJoin below for further details. 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 ==== QQueryFilter
A key component of *<<QueryInput>>*, 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`). A key component of *<<QueryInput>>*, 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 include its own additional *subFilters*.
** Each *subFilter* can specify a different *booleanOperator*. ** Each *subFilter* can specify a different *booleanOperator*.
** For example, consider the following *QQueryFilter*, that uses two *subFilters*, and a mix of *booleanOperators* ** 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] [source,java]
---- ----

View File

@ -26,6 +26,7 @@ import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; 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.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; 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.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.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; 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.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash; import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils; 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")); throw (new QException("A table named [" + queryInput.getTableName() + "] was not found in the active QInstance"));
} }
validateFieldNamesToInclude(queryInput);
QBackendMetaData backend = queryInput.getBackend(); QBackendMetaData backend = queryInput.getBackend();
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_QUERY_RECORD.getRole()); postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(table, TableCustomizers.POST_QUERY_RECORD.getRole());
this.queryInput = queryInput; 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<String> 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<String> unrecognizedFieldNames = new ArrayList<>();
Map<String, QTableMetaData> 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 ** 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. ** records to be returned, and you just want to pass in a table name and filter.

View File

@ -66,6 +66,14 @@ public class QueryInput extends AbstractTableActionInput implements QueryOrGetIn
private List<QueryJoin> queryJoins = null; private List<QueryJoin> queryJoins = null;
private boolean selectDistinct = false; 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<String> fieldNamesToInclude;
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if you say you want to includeAssociations, you can limit which ones by passing them in associationNamesToInclude. // // 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. // // 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)); return (queryHints.contains(queryHint));
} }
/*******************************************************************************
** Getter for fieldNamesToInclude
*******************************************************************************/
public Set<String> getFieldNamesToInclude()
{
return (this.fieldNamesToInclude);
}
/*******************************************************************************
** Setter for fieldNamesToInclude
*******************************************************************************/
public void setFieldNamesToInclude(Set<String> fieldNamesToInclude)
{
this.fieldNamesToInclude = fieldNamesToInclude;
}
/*******************************************************************************
** Fluent setter for fieldNamesToInclude
*******************************************************************************/
public QueryInput withFieldNamesToInclude(Set<String> fieldNamesToInclude)
{
this.fieldNamesToInclude = fieldNamesToInclude;
return (this);
}
} }

View File

@ -24,6 +24,8 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import com.kingsrook.qqq.backend.core.BaseTest; 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.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; 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.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.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; 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 com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull; 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()); 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");
}
} }

View File

@ -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.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; 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.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.actions.tables.query.expressions.AbstractFilterExpression;
import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -176,18 +177,28 @@ public class AbstractMongoDBAction
/******************************************************************************* /*******************************************************************************
** Convert a mongodb document to a QRecord. ** Convert a mongodb document to a QRecord.
*******************************************************************************/ *******************************************************************************/
protected QRecord documentToRecord(QTableMetaData table, Document document) protected QRecord documentToRecord(QueryInput queryInput, Document document)
{ {
QTableMetaData table = queryInput.getTable();
QRecord record = new QRecord(); QRecord record = new QRecord();
record.setTableName(table.getName()); record.setTableName(table.getName());
/////////////////////////////////////////////
// build the set of field names to include //
/////////////////////////////////////////////
Set<String> fieldNamesToInclude = queryInput.getFieldNamesToInclude();
List<QFieldMetaData> 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, // // 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 // // 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 // // as we go - then after this loop, will handle all remaining values as unstructured fields //
////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////
Map<String, Serializable> values = record.getValues(); Map<String, Serializable> values = record.getValues();
for(QFieldMetaData field : table.getFields().values()) for(QFieldMetaData field : selectedFields)
{ {
String fieldName = field.getName(); String fieldName = field.getName();
String fieldBackendName = getFieldBackendName(field); String fieldBackendName = getFieldBackendName(field);

View File

@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.module.mongodb.model.metadata.MongoDBBackendMet
import com.mongodb.client.FindIterable; import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Projections;
import org.bson.Document; import org.bson.Document;
import org.bson.conversions.Bson; import org.bson.conversions.Bson;
@ -96,6 +97,15 @@ public class MongoDBQueryAction extends AbstractMongoDBAction implements QueryIn
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
FindIterable<Document> cursor = collection.find(mongoClientContainer.getMongoSession(), searchQuery); FindIterable<Document> 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<String> backendFieldNames = queryInput.getFieldNamesToInclude().stream().map(f -> getFieldBackendName(table.getField(f))).toList();
cursor.projection(Projections.include(backendFieldNames));
}
/////////////////////////////////// ///////////////////////////////////
// add a sort operator if needed // // add a sort operator if needed //
/////////////////////////////////// ///////////////////////////////////
@ -138,7 +148,7 @@ public class MongoDBQueryAction extends AbstractMongoDBAction implements QueryIn
actionTimeoutHelper.cancel(); actionTimeoutHelper.cancel();
setQueryStatFirstResultTime(); setQueryStatFirstResultTime();
QRecord record = documentToRecord(table, document); QRecord record = documentToRecord(queryInput, document);
queryOutput.addRecord(record); queryOutput.addRecord(record);
if(queryInput.getAsyncJobCallback().wasCancelRequested()) if(queryInput.getAsyncJobCallback().wasCancelRequested())

View File

@ -29,6 +29,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; 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.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext; 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 org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; 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)); .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"));
}
} }

View File

@ -33,10 +33,12 @@ import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; 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.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.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
@ -92,7 +94,8 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf
QTableMetaData table = queryInput.getTable(); QTableMetaData table = queryInput.getTable();
String tableName = queryInput.getTableName(); 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()); QQueryFilter filter = clonedOrNewFilter(queryInput.getFilter());
JoinsContext joinsContext = new JoinsContext(queryInput.getInstance(), tableName, queryInput.getQueryJoins(), filter); JoinsContext joinsContext = new JoinsContext(queryInput.getInstance(), tableName, queryInput.getQueryJoins(), filter);
@ -133,23 +136,6 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf
needToCloseConnection = true; needToCloseConnection = true;
} }
////////////////////////////////////////////////////////////////////////////
// build the list of fields that will be processed in the result-set loop //
////////////////////////////////////////////////////////////////////////////
List<QFieldMetaData> 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(); Long mark = System.currentTimeMillis();
try try
@ -189,7 +175,7 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf
for(int i = 1; i <= metaData.getColumnCount(); i++) 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()) if(!queryInput.getShouldFetchHeavyFields() && field.getIsHeavy())
{ {
@ -284,26 +270,60 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf
/******************************************************************************* /***************************************************************************
** ** output wrapper for makeSelection method.
*******************************************************************************/ ** - selectClause is everything from SELECT up to (but not including) FROM
private String makeSelectClause(QueryInput queryInput) throws QException ** - fields are those being selected, in the same order, and with mutated
** names for join fields.
***************************************************************************/
private record Selection(String selectClause, List<QFieldMetaData> 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(); String tableName = queryInput.getTableName();
List<QueryJoin> queryJoins = queryInput.getQueryJoins(); List<QueryJoin> queryJoins = queryInput.getQueryJoins();
QTableMetaData table = instance.getTable(tableName); QTableMetaData table = instance.getTable(tableName);
boolean requiresDistinct = queryInput.getSelectDistinct() || doesSelectClauseRequireDistinct(table); Set<String> fieldNamesToInclude = queryInput.getFieldNamesToInclude();
String clausePrefix = (requiresDistinct) ? "SELECT DISTINCT " : "SELECT ";
List<QFieldMetaData> fieldList = new ArrayList<>(table.getFields().values()); ///////////////////////////////////////////////////////////////////////////////////////////////
// start with the main table's fields, optionally filtered by the set of fieldNamesToInclude //
///////////////////////////////////////////////////////////////////////////////////////////////
List<QFieldMetaData> 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() String columns = fieldList.stream()
.map(field -> Pair.of(field, escapeIdentifier(tableName) + "." + escapeIdentifier(getColumnName(field)))) .map(field -> Pair.of(field, escapeIdentifier(tableName) + "." + escapeIdentifier(getColumnName(field))))
.map(pair -> wrapHeavyFieldsWithLengthFunctionIfNeeded(pair, queryInput.getShouldFetchHeavyFields())) .map(pair -> wrapHeavyFieldsWithLengthFunctionIfNeeded(pair, queryInput.getShouldFetchHeavyFields()))
.collect(Collectors.joining(", ")); .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<QFieldMetaData> selectionFieldList = new ArrayList<>(fieldList);
///////////////////////////////////
// add any 'selected' queryJoins //
///////////////////////////////////
for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins)) for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins))
{ {
if(queryJoin.getSelect()) 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."); throw new QException("Requested join table [" + queryJoin.getJoinTable() + "] is not a defined table.");
} }
List<QFieldMetaData> joinFieldList = new ArrayList<>(joinTable.getFields().values()); ///////////////////////////////////
// filter by fieldNamesToInclude //
///////////////////////////////////
List<QFieldMetaData> 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() String joinColumns = joinFieldList.stream()
.map(field -> Pair.of(field, escapeIdentifier(tableNameOrAlias) + "." + escapeIdentifier(getColumnName(field)))) .map(field -> Pair.of(field, escapeIdentifier(tableNameOrAlias) + "." + escapeIdentifier(getColumnName(field))))
.map(pair -> wrapHeavyFieldsWithLengthFunctionIfNeeded(pair, queryInput.getShouldFetchHeavyFields())) .map(pair -> wrapHeavyFieldsWithLengthFunctionIfNeeded(pair, queryInput.getShouldFetchHeavyFields()))
.collect(Collectors.joining(", ")); .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));
} }

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.module.rdbms.actions;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; 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()); 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);
}
} }

View File

@ -28,6 +28,7 @@ import java.time.temporal.ChronoUnit;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; 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.core.model.session.QSession;
import com.kingsrook.qqq.backend.module.rdbms.TestUtils; import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); 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)))); .withValues(List.of(1_000_000))));
queryOutput = new RDBMSQueryAction().execute(queryInput); queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(4, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(5, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(3, queryOutput.getRecords().size(), "Expected # of rows"); 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); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); 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())))); .withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.LESS_THAN).withValues(List.of(new Now()))));
QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); 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))))); .withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.LESS_THAN).withValues(List.of(NowWithOffset.plus(2, ChronoUnit.DAYS)))));
QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size(), "Expected # of rows"); 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))))); .withCriteria(new QFilterCriteria().withFieldName("birthDate").withOperator(QCriteriaOperator.GREATER_THAN).withValues(List.of(NowWithOffset.minus(5, ChronoUnit.DAYS)))));
QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput); QueryOutput queryOutput = new RDBMSQueryAction().execute(queryInput);
assertEquals(2, queryOutput.getRecords().size(), "Expected # of rows"); assertEquals(2, 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");
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("future")), "Should find expected row");
} }
} }
@ -1004,7 +1006,36 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
queryInput.setShouldFetchHeavyFields(true); queryInput.setShouldFetchHeavyFields(true);
records = new QueryAction().execute(queryInput).getRecords(); 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); 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());
} }
} }