Skip to content

[APP-367] feat: Update subset_sql to use basic filters from request#3160

Open
emyl3 wants to merge 36 commits intomainfrom
el/app-367
Open

[APP-367] feat: Update subset_sql to use basic filters from request#3160
emyl3 wants to merge 36 commits intomainfrom
el/app-367

Conversation

@emyl3
Copy link
Copy Markdown
Contributor

@emyl3 emyl3 commented Apr 13, 2026

Description

The PR takes basic filters from a ReportConfiguration then creates a WHERE clause and adds that to the subset_sql. The funcationality has been pulled from buildWhereClause() in ReportSQLBuilder. This only implements the basic filters.

Tickets

Checklist before requesting a review

  • PR focuses on a single story
  • Code has been fully tested to meet acceptance criteria
  • PR is reasonably small and reviewable (Generally less than 10 files and 500 changed lines)
  • All new functions/classes/components reasonably small
  • Functions/classes/components focused on one responsibility
  • Code easy to understand and modify (clarity over concise/clever)
  • PRs containing TypeScript follow the Do's and Don'ts
  • PR does not contain hardcoded values (Uses constants)
  • All code is covered by unit or feature tests

@brick-green brick-green changed the title VERY WIP Handoff - [APP-367] feat: Update subset_sql to use basic filters from request APP-367] feat: Update subset_sql to use basic filters from request Apr 22, 2026
@brick-green brick-green changed the title APP-367] feat: Update subset_sql to use basic filters from request [APP-367] feat: Update subset_sql to use basic filters from request Apr 22, 2026
@brick-green brick-green marked this pull request as ready for review April 22, 2026 15:15
@brick-green brick-green requested a review from a team as a code owner April 22, 2026 15:15
@brick-green brick-green requested review from JordanGuinn and mcmcgrath13 and removed request for a team April 22, 2026 15:15
public class FieldFormatter {
public String formatField(String type, String value) {
return switch (type.toUpperCase()) {
case "STRING" -> "'" + value.replace("'", "''") + "'";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to do any other sql escaping?


for (FilterConfiguration filter : reportConfig.filters()) {
findColumn(reportConfig, filter)
.ifPresent(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about building filters that don't have columns? do all basic filters target a column?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current NBS6 code callsgetDataSourceColumnDT() as one of the first steps. That method throws an exception if a column is not found. I was just working from that. Is that a safe route?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so. The advanced filter doesn't have a column, but I'm pretty sure every basic filter does and since this is for basic an error makes sense

column -> {
String segment = buildFilterFragment(filter, column);
if (!segment.isEmpty()) finalWhere.add(segment);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(q, b): should there be an error if there's no column?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, I'll change to throw an exception

Comment thread apps/modernization-api/src/main/java/gov/cdc/nbs/report/WhereClauseService.java Outdated
Comment thread apps/modernization-api/src/main/java/gov/cdc/nbs/report/WhereClauseService.java Outdated
Copy link
Copy Markdown
Contributor

@mcmcgrath13 mcmcgrath13 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall structure looks good, but need to fix up the data source for the filter building

@brick-green brick-green marked this pull request as draft April 23, 2026 16:43
# Conflicts:
#	apps/modernization-api/src/main/java/gov/cdc/nbs/report/ReportService.java
#	apps/modernization-api/src/main/java/gov/cdc/nbs/report/models/AdvancedFilterConfiguration.java
#	apps/modernization-ui/src/generated/models/BasicFilterConfiguration.ts

StringBuilder segment = new StringBuilder("(");
String colName = "[" + column.columnName() + "]"; // Brackets protect against SQL reserved words
boolean hasValues = !formattedValues.isEmpty();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(q, nb): when could this be empty given the input values is known to not be empty?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth throwing if there are somehow no formatted values? Or if the number of formatted values somehow doesn't match the number of values on the request (which should never happen, but yannow)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throwing an exception here now for both the scenarios

@brick-green brick-green requested a review from mcmcgrath13 May 6, 2026 19:12
finalWhere.add(basicWherefragment);

// Only return the WHERE clause if it contains anything beyond the initial "WHERE " prefix
return finalWhere.length() > 6 ? finalWhere.toString() : "";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit, nb): We could also check to see if basicWherefragment returns a non-empty value? Not a huge deal by any means, just seems a bit precarious to check against the base WHERE clause length.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my thought was a buildAdvancedWhereFragment() would be added next so that check on the WHERE length would cover for both. What do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh, sure that makes sense. I suppose so long as we have tests covering both those cases, and the resulting where clause output, that should be fine!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit, nb): Thoughts on naming this WhereClauseBuilder or similar, in the spirit of following the pattern of ReportSpecBuilder?

} catch (DateTimeParseException e) {
throw new IllegalArgumentException("Can't Convert Date: " + date);
}
// Produces '2023-12-31'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(q, nb): Is this true? It's not the output I would expect with us parsing to "MM/dd/yyyy"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the default SQL Server date format. So it seemed like the best format to converge on for building the SQL criteria


StringBuilder segment = new StringBuilder("(");
String colName = "[" + column.columnName() + "]"; // Brackets protect against SQL reserved words
boolean hasValues = !formattedValues.isEmpty();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth throwing if there are somehow no formatted values? Or if the number of formatted values somehow doesn't match the number of values on the request (which should never happen, but yannow)


// Malicious Input: Attempt to close the IN clause and drop the Reports table
// The -- at the end comments out the rest of your generated query (the closing parentheses)
String maliciousInput = "'); DROP TABLE Reports; --";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏 📈

@@ -30,8 +31,15 @@ public static BasicFilterConfiguration fromReportFilter(ReportFilter filter) {
// For the future: A list of strings may end up being too simple for all use cases,
// may need to evolve to be a small object with a key and value
List<String> defaultValue = null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
List<String> defaultValue = null;
List<String> defaultValues = null;

@Schema(requiredMode = Schema.RequiredMode.REQUIRED) Boolean isRequired,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) FilterType filterType) {}
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) FilterType filterType,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) Boolean defaultIncludeNulls) {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit, nb): the minorest of baby nits, but I'd probably put this field just after defaultValues for colocation of sort of thing reasons


StringBuilder criteria = new StringBuilder("(");

String colName = "[" + column.columnName() + "]";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(sugg, nb): probably overkill, but do we want a utility somewhere for escaping/formatting column Names? We do it here and elsewhere in the query building

Comment on lines +27 to +28
assertThat(formatter.formatField("INTEGER", "123")).isEqualTo("123");
assertThat(formatter.formatField("NUMBER", "45.67")).isEqualTo("45.67");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(sugg, b): for security, can we ensure that the numbers are number shaped before allowing them through?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call! Added

assertThat(monthResult.get(1)).isEqualTo("'2024-02-29'"); // Leap year check

// Test yyyy Range
List<String> yearRange = List.of("2023", "2023");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
List<String> yearRange = List.of("2023", "2023");
List<String> yearRange = List.of("2023", "2024");

to stress test leap years

@Test
void should_throw_exception_for_invalid_range_size() {
List<String> tooShort = List.of("2023");
assertThatThrownBy(() -> formatter.convertToSQLFromDateRange(tooShort))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we also test too long?

String whereFragment =
mockWhereClauseService.buildBasicWhereFragment(reportConfig, basicFilterRequest);

assertThat(whereFragment).isEqualTo("([ColumnName] IN (condition1))");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assertThat(whereFragment).isEqualTo("([ColumnName] IN (condition1))");
assertThat(whereFragment).isEqualTo("([ColumnName] IN ('condition1'))");

Comment on lines +36 to +40
mockWhereClauseService = new WhereClauseService(fieldFormatter);
// Default behavior for formatter: return value as is
Mockito.lenient()
.when(fieldFormatter.formatField(anyString(), anyString()))
.thenAnswer(ff -> ff.getArgument(1));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(q, nb): why do we need to mock the where clause service? it's stateless so couldn't we just use it? My gut-code-smell still doesn't really grok why the where clause service is a service and not just a class that's normally imported and used where it needs using (and then we wouldn't mock it, but just use it) - can you eli5?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same with the field formatter - seems like those both could be static classes

import java.util.List;
import org.springframework.stereotype.Component;

@Component
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(q, nb): why have this be a component instead of just a regular utility class?

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 8, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants