Skip to content

Commit 1679178

Browse files
committed
feat(memoryBackend): add feature flag to build cross-products more correctly for queries that require joins for security fields.
In theory this should eventually become the default behavior, but it does change behavior (even if the change is more correct), so this initial commit puts the new behavior behind a feature flag.
1 parent 27445ef commit 1679178

File tree

1 file changed

+81
-6
lines changed
  • qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory

1 file changed

+81
-6
lines changed

qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ public class MemoryRecordStore
112112

113113
public static final ListingHash<Class<? extends AbstractActionInput>, AbstractActionInput> actionInputs = new ListingHash<>();
114114

115+
//////////////////////////////////////////////////////////////////////
116+
// to slow-roll this change in capability, set it as a feature-flag //
117+
// on the MemoryRecordStore singleton instance. This should allow //
118+
// an individual test, for example, to toggle it, then reset it. //
119+
//////////////////////////////////////////////////////////////////////
120+
public static boolean BUILD_JOIN_CROSS_PRODUCT_FROM_JOIN_CONTEXT_DEFAULT = false;
121+
private boolean buildJoinCrossProductFromJoinContext = BUILD_JOIN_CROSS_PRODUCT_FROM_JOIN_CONTEXT_DEFAULT;
115122

116123

117124
/*******************************************************************************
@@ -206,9 +213,24 @@ public List<QRecord> query(QueryInput input) throws QException
206213

207214
QQueryFilter filter = clonedOrNewFilter(input.getFilter());
208215
JoinsContext joinsContext = new JoinsContext(QContext.getQInstance(), input.getTableName(), input.getQueryJoins(), filter);
209-
if(CollectionUtils.nullSafeHasContents(input.getQueryJoins()))
216+
217+
/////////////////////////////////////////////////////////////////////////////////////////////////
218+
// if we every wanted or needed per-query control here, that could look like: //
219+
// || input.hasFlag(MemoryBackendQueryActionFlags.BUILD_JOIN_CROSS_PRODUCT_FROM_JOIN_CONTEXT)) //
220+
/////////////////////////////////////////////////////////////////////////////////////////////////
221+
if(buildJoinCrossProductFromJoinContext)
222+
{
223+
if(CollectionUtils.nullSafeHasContents(joinsContext.getQueryJoins()))
224+
{
225+
tableData = buildJoinCrossProduct(input.getTable(), joinsContext.getQueryJoins());
226+
}
227+
}
228+
else
210229
{
211-
tableData = buildJoinCrossProduct(input);
230+
if(CollectionUtils.nullSafeHasContents(input.getQueryJoins()))
231+
{
232+
tableData = buildJoinCrossProduct(input.getTable(), input.getQueryJoins());
233+
}
212234
}
213235

214236
///////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -273,20 +295,20 @@ public List<QRecord> query(QueryInput input) throws QException
273295
/*******************************************************************************
274296
**
275297
*******************************************************************************/
276-
private Collection<QRecord> buildJoinCrossProduct(QueryInput input) throws QException
298+
private Collection<QRecord> buildJoinCrossProduct(QTableMetaData table, List<QueryJoin> queryJoins) throws QException
277299
{
278300
QInstance qInstance = QContext.getQInstance();
279301

280302
List<QRecord> crossProduct = new ArrayList<>();
281-
QTableMetaData leftTable = input.getTable();
303+
QTableMetaData leftTable = table;
282304
for(QRecord record : getTableData(leftTable).values())
283305
{
284306
QRecord productRecord = new QRecord();
285307
addRecordToProduct(productRecord, record, null);
286308
crossProduct.add(productRecord);
287309
}
288310

289-
for(QueryJoin queryJoin : input.getQueryJoins())
311+
for(QueryJoin queryJoin : queryJoins)
290312
{
291313
QTableMetaData nextTable = qInstance.getTable(queryJoin.getJoinTable());
292314
Collection<QRecord> nextTableRecords = getTableData(nextTable).values();
@@ -309,7 +331,11 @@ private Collection<QRecord> buildJoinCrossProduct(QueryInput input) throws QExce
309331

310332
if(!matchFound)
311333
{
312-
// todo - Left & Right joins
334+
if(QueryJoin.Type.LEFT.equals(queryJoin.getType()))
335+
{
336+
QRecord joinRecord = new QRecord(productRecord);
337+
nextLevelProduct.add(joinRecord);
338+
}
313339
}
314340
}
315341

@@ -1186,4 +1212,53 @@ private record Variant(String type, Serializable id) implements BackendIdentifie
11861212
{
11871213
}
11881214

1215+
1216+
1217+
/*******************************************************************************
1218+
* Getter for buildJoinCrossProductFromJoinContext
1219+
* @see #withBuildJoinCrossProductFromJoinContext(boolean)
1220+
*******************************************************************************/
1221+
public boolean getBuildJoinCrossProductFromJoinContext()
1222+
{
1223+
return (this.buildJoinCrossProductFromJoinContext);
1224+
}
1225+
1226+
1227+
1228+
/*******************************************************************************
1229+
* Setter for buildJoinCrossProductFromJoinContext
1230+
* @see #withBuildJoinCrossProductFromJoinContext(boolean)
1231+
*******************************************************************************/
1232+
public void setBuildJoinCrossProductFromJoinContext(boolean buildJoinCrossProductFromJoinContext)
1233+
{
1234+
this.buildJoinCrossProductFromJoinContext = buildJoinCrossProductFromJoinContext;
1235+
}
1236+
1237+
1238+
1239+
/*******************************************************************************
1240+
* Fluent setter for buildJoinCrossProductFromJoinContext
1241+
*
1242+
* @param buildJoinCrossProductFromJoinContext
1243+
* The original implementation of this class only tried to build cross-products
1244+
* for joins based on QueryJoin objects directly added to the QueryInput.
1245+
* However, this meant that if a join was needed for the security key on a table,
1246+
* that this table wouldn't be included in the join cross product, thus incorrect
1247+
* query results could be returned.
1248+
*
1249+
* <p>This new behavior (to use joins from the JoinContext, which means, it includes
1250+
* joins needed for security) seems like the obviously more correct way to do it -
1251+
* but, it is a potentially breaking change, so we want to slow-roll it out.
1252+
* Thus, an application (or a unit test) can opt-in to the new behavior by
1253+
* setting this field to true on a MemoryRecordStore singleton instance.</p>
1254+
*
1255+
* @return this
1256+
*******************************************************************************/
1257+
public MemoryRecordStore withBuildJoinCrossProductFromJoinContext(boolean buildJoinCrossProductFromJoinContext)
1258+
{
1259+
this.buildJoinCrossProductFromJoinContext = buildJoinCrossProductFromJoinContext;
1260+
return (this);
1261+
}
1262+
1263+
11891264
}

0 commit comments

Comments
 (0)