mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 05:01:07 +00:00
Search for join meta data through exposed joins.
This commit is contained in:
@ -30,9 +30,12 @@ import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
@ -203,7 +206,8 @@ public abstract class AbstractRDBMSAction implements QActionInterface
|
||||
{
|
||||
StringBuilder rs = new StringBuilder(escapeIdentifier(getTableName(instance.getTable(tableName))) + " AS " + escapeIdentifier(tableName));
|
||||
|
||||
for(QueryJoin queryJoin : joinsContext.getQueryJoins())
|
||||
List<QueryJoin> queryJoins = sortQueryJoinsForFromClause(tableName, joinsContext.getQueryJoins());
|
||||
for(QueryJoin queryJoin : queryJoins)
|
||||
{
|
||||
QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
|
||||
String tableNameOrAlias = queryJoin.getJoinTableOrItsAlias();
|
||||
@ -264,6 +268,48 @@ public abstract class AbstractRDBMSAction implements QActionInterface
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** We've seen some SQL dialects (mysql, but not h2...) be unhappy if we have
|
||||
** the from/join-ons out of order. This method resorts the joins, to start with
|
||||
** main table, then any tables attached to it, then fanning out from there.
|
||||
*******************************************************************************/
|
||||
private List<QueryJoin> sortQueryJoinsForFromClause(String mainTableName, List<QueryJoin> queryJoins)
|
||||
{
|
||||
List<QueryJoin> inputListCopy = new ArrayList<>(queryJoins);
|
||||
|
||||
List<QueryJoin> rs = new ArrayList<>();
|
||||
Set<String> seenTables = new HashSet<>();
|
||||
seenTables.add(mainTableName);
|
||||
|
||||
boolean keepGoing = true;
|
||||
while(!inputListCopy.isEmpty() && keepGoing)
|
||||
{
|
||||
keepGoing = false;
|
||||
Iterator<QueryJoin> iterator = inputListCopy.iterator();
|
||||
while(iterator.hasNext())
|
||||
{
|
||||
QueryJoin next = iterator.next();
|
||||
if((StringUtils.hasContent(next.getBaseTableOrAlias()) && seenTables.contains(next.getBaseTableOrAlias())) || seenTables.contains(next.getJoinTable()))
|
||||
{
|
||||
rs.add(next);
|
||||
if(StringUtils.hasContent(next.getBaseTableOrAlias()))
|
||||
{
|
||||
seenTables.add(next.getBaseTableOrAlias());
|
||||
}
|
||||
seenTables.add(next.getJoinTable());
|
||||
iterator.remove();
|
||||
keepGoing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rs.addAll(inputListCopy);
|
||||
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** method that sub-classes should call to make a full WHERE clause, including
|
||||
** security clauses.
|
||||
@ -902,6 +948,16 @@ public abstract class AbstractRDBMSAction implements QActionInterface
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Make it easy (e.g., for tests) to turn on logging of SQL
|
||||
*******************************************************************************/
|
||||
public static void setLogSQLOutput(String loggerOrSystemOut)
|
||||
{
|
||||
System.setProperty("qqq.rdbms.logSQL.output", loggerOrSystemOut);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -44,6 +44,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSActionTest;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
|
||||
@ -239,6 +240,7 @@ public class TestUtils
|
||||
qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER, "order")
|
||||
.withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("storeId"))
|
||||
.withAssociation(new Association().withName("orderLine").withAssociatedTableName(TABLE_NAME_ORDER_LINE).withJoinName("orderJoinOrderLine"))
|
||||
.withExposedJoin(new ExposedJoin().withJoinTable(TABLE_NAME_ITEM).withJoinPath(List.of("orderJoinOrderLine", "orderLineJoinItem")))
|
||||
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE))
|
||||
.withField(new QFieldMetaData("billToPersonId", QFieldType.INTEGER).withBackendName("bill_to_person_id").withPossibleValueSourceName(TABLE_NAME_PERSON))
|
||||
.withField(new QFieldMetaData("shipToPersonId", QFieldType.INTEGER).withBackendName("ship_to_person_id").withPossibleValueSourceName(TABLE_NAME_PERSON))
|
||||
@ -246,7 +248,9 @@ public class TestUtils
|
||||
|
||||
qInstance.addTable(defineBaseTable(TABLE_NAME_ITEM, "item")
|
||||
.withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("storeId"))
|
||||
.withExposedJoin(new ExposedJoin().withJoinTable(TABLE_NAME_ORDER).withJoinPath(List.of("orderLineJoinItem", "orderJoinOrderLine")))
|
||||
.withField(new QFieldMetaData("sku", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("description", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE))
|
||||
);
|
||||
|
||||
|
@ -51,6 +51,7 @@ 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.utils.StringUtils;
|
||||
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;
|
||||
@ -72,6 +73,20 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
|
||||
public void beforeEach() throws Exception
|
||||
{
|
||||
super.primeTestDatabase();
|
||||
|
||||
// AbstractRDBMSAction.setLogSQL(true);
|
||||
// AbstractRDBMSAction.setLogSQLOutput("system.out");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@AfterEach
|
||||
void afterEach()
|
||||
{
|
||||
AbstractRDBMSAction.setLogSQL(false);
|
||||
}
|
||||
|
||||
|
||||
@ -1103,6 +1118,149 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Given tables:
|
||||
** order - orderLine - item
|
||||
** with exposedJoin on order to item
|
||||
** do a query on order, also selecting item.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testTwoTableAwayExposedJoin() throws QException
|
||||
{
|
||||
QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
|
||||
|
||||
QInstance instance = TestUtils.defineInstance();
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
|
||||
queryInput.withQueryJoins(List.of(
|
||||
new QueryJoin(TestUtils.TABLE_NAME_ITEM).withType(QueryJoin.Type.INNER).withSelect(true)
|
||||
));
|
||||
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
List<QRecord> records = queryOutput.getRecords();
|
||||
assertThat(records).hasSize(11); // one per line item
|
||||
assertThat(records).allMatch(r -> r.getValue("id") != null);
|
||||
assertThat(records).allMatch(r -> r.getValue(TestUtils.TABLE_NAME_ITEM + ".description") != null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Given tables:
|
||||
** order - orderLine - item
|
||||
** with exposedJoin on item to order
|
||||
** do a query on item, also selecting order.
|
||||
** This is a reverse of the above, to make sure join flipping, etc, is good.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testTwoTableAwayExposedJoinReversed() throws QException
|
||||
{
|
||||
QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
|
||||
|
||||
QInstance instance = TestUtils.defineInstance();
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(TestUtils.TABLE_NAME_ITEM);
|
||||
|
||||
queryInput.withQueryJoins(List.of(
|
||||
new QueryJoin(TestUtils.TABLE_NAME_ORDER).withType(QueryJoin.Type.INNER).withSelect(true)
|
||||
));
|
||||
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
List<QRecord> records = queryOutput.getRecords();
|
||||
assertThat(records).hasSize(11); // one per line item
|
||||
assertThat(records).allMatch(r -> r.getValue("description") != null);
|
||||
assertThat(records).allMatch(r -> r.getValue(TestUtils.TABLE_NAME_ORDER + ".id") != null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Given tables:
|
||||
** order - orderLine - item
|
||||
** with exposedJoin on order to item
|
||||
** do a query on order, also selecting item, and also selecting orderLine...
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testTwoTableAwayExposedJoinAlsoSelectingInBetweenTable() throws QException
|
||||
{
|
||||
QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
|
||||
|
||||
QInstance instance = TestUtils.defineInstance();
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
|
||||
queryInput.withQueryJoins(List.of(
|
||||
new QueryJoin(TestUtils.TABLE_NAME_ORDER_LINE).withType(QueryJoin.Type.INNER).withSelect(true),
|
||||
new QueryJoin(TestUtils.TABLE_NAME_ITEM).withType(QueryJoin.Type.INNER).withSelect(true)
|
||||
));
|
||||
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
List<QRecord> records = queryOutput.getRecords();
|
||||
assertThat(records).hasSize(11); // one per line item
|
||||
assertThat(records).allMatch(r -> r.getValue("id") != null);
|
||||
assertThat(records).allMatch(r -> r.getValue(TestUtils.TABLE_NAME_ORDER_LINE + ".quantity") != null);
|
||||
assertThat(records).allMatch(r -> r.getValue(TestUtils.TABLE_NAME_ITEM + ".description") != null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Given tables:
|
||||
** order - orderLine - item
|
||||
** with exposedJoin on order to item
|
||||
** do a query on order, filtered by item
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testTwoTableAwayExposedJoinWhereClauseOnly() throws QException
|
||||
{
|
||||
QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
|
||||
|
||||
QInstance instance = TestUtils.defineInstance();
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(TestUtils.TABLE_NAME_ITEM + ".description", QCriteriaOperator.STARTS_WITH, "Q-Mart")));
|
||||
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
List<QRecord> records = queryOutput.getRecords();
|
||||
assertThat(records).hasSize(4);
|
||||
assertThat(records).allMatch(r -> r.getValue("id") != null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Given tables:
|
||||
** order - orderLine - item
|
||||
** with exposedJoin on order to item
|
||||
** do a query on order, filtered by item
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testTwoTableAwayExposedJoinWhereClauseBothJoinTables() throws QException
|
||||
{
|
||||
QContext.setQSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
|
||||
|
||||
QInstance instance = TestUtils.defineInstance();
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
queryInput.setFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria(TestUtils.TABLE_NAME_ITEM + ".description", QCriteriaOperator.STARTS_WITH, "Q-Mart"))
|
||||
.withCriteria(new QFilterCriteria(TestUtils.TABLE_NAME_ORDER_LINE + ".quantity", QCriteriaOperator.IS_NOT_BLANK))
|
||||
);
|
||||
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
List<QRecord> records = queryOutput.getRecords();
|
||||
assertThat(records).hasSize(4);
|
||||
assertThat(records).allMatch(r -> r.getValue("id") != null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** queries on the store table, where the primary key (id) is the security field
|
||||
*******************************************************************************/
|
||||
|
@ -102,19 +102,20 @@ CREATE TABLE item
|
||||
(
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
sku VARCHAR(80) NOT NULL,
|
||||
description VARCHAR(80),
|
||||
store_id INT NOT NULL REFERENCES store
|
||||
);
|
||||
|
||||
-- three items for each store
|
||||
INSERT INTO item (id, sku, store_id) VALUES (1, 'QM-1', 1);
|
||||
INSERT INTO item (id, sku, store_id) VALUES (2, 'QM-2', 1);
|
||||
INSERT INTO item (id, sku, store_id) VALUES (3, 'QM-3', 1);
|
||||
INSERT INTO item (id, sku, store_id) VALUES (4, 'QRU-1', 2);
|
||||
INSERT INTO item (id, sku, store_id) VALUES (5, 'QRU-2', 2);
|
||||
INSERT INTO item (id, sku, store_id) VALUES (6, 'QRU-3', 2);
|
||||
INSERT INTO item (id, sku, store_id) VALUES (7, 'QD-1', 3);
|
||||
INSERT INTO item (id, sku, store_id) VALUES (8, 'QD-2', 3);
|
||||
INSERT INTO item (id, sku, store_id) VALUES (9, 'QD-3', 3);
|
||||
INSERT INTO item (id, sku, description, store_id) VALUES (1, 'QM-1', 'Q-Mart Item 1', 1);
|
||||
INSERT INTO item (id, sku, description, store_id) VALUES (2, 'QM-2', 'Q-Mart Item 2', 1);
|
||||
INSERT INTO item (id, sku, description, store_id) VALUES (3, 'QM-3', 'Q-Mart Item 3', 1);
|
||||
INSERT INTO item (id, sku, description, store_id) VALUES (4, 'QRU-1', 'QQQ R Us Item 4', 2);
|
||||
INSERT INTO item (id, sku, description, store_id) VALUES (5, 'QRU-2', 'QQQ R Us Item 5', 2);
|
||||
INSERT INTO item (id, sku, description, store_id) VALUES (6, 'QRU-3', 'QQQ R Us Item 6', 2);
|
||||
INSERT INTO item (id, sku, description, store_id) VALUES (7, 'QD-1', 'QDepot Item 7', 3);
|
||||
INSERT INTO item (id, sku, description, store_id) VALUES (8, 'QD-2', 'QDepot Item 8', 3);
|
||||
INSERT INTO item (id, sku, description, store_id) VALUES (9, 'QD-3', 'QDepot Item 9', 3);
|
||||
|
||||
CREATE TABLE `order`
|
||||
(
|
||||
|
Reference in New Issue
Block a user