[APP-367] feat: Update subset_sql to use basic filters from request#3160
[APP-367] feat: Update subset_sql to use basic filters from request#3160
Conversation
# Conflicts: # apps/modernization-api/src/main/java/gov/cdc/nbs/report/ReportService.java
| public class FieldFormatter { | ||
| public String formatField(String type, String value) { | ||
| return switch (type.toUpperCase()) { | ||
| case "STRING" -> "'" + value.replace("'", "''") + "'"; |
There was a problem hiding this comment.
do we need to do any other sql escaping?
|
|
||
| for (FilterConfiguration filter : reportConfig.filters()) { | ||
| findColumn(reportConfig, filter) | ||
| .ifPresent( |
There was a problem hiding this comment.
what about building filters that don't have columns? do all basic filters target a column?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); | ||
| }); |
There was a problem hiding this comment.
(q, b): should there be an error if there's no column?
There was a problem hiding this comment.
yup, I'll change to throw an exception
mcmcgrath13
left a comment
There was a problem hiding this comment.
Overall structure looks good, but need to fix up the data source for the filter building
# 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(); |
There was a problem hiding this comment.
(q, nb): when could this be empty given the input values is known to not be empty?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
throwing an exception here now for both the scenarios
| finalWhere.add(basicWherefragment); | ||
|
|
||
| // Only return the WHERE clause if it contains anything beyond the initial "WHERE " prefix | ||
| return finalWhere.length() > 6 ? finalWhere.toString() : ""; |
There was a problem hiding this comment.
(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.
There was a problem hiding this comment.
my thought was a buildAdvancedWhereFragment() would be added next so that check on the WHERE length would cover for both. What do you think?
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
(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' |
There was a problem hiding this comment.
(q, nb): Is this true? It's not the output I would expect with us parsing to "MM/dd/yyyy"
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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; --"; |
| @@ -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; | |||
There was a problem hiding this comment.
| 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) {} |
There was a problem hiding this comment.
(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() + "]"; |
There was a problem hiding this comment.
(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
| assertThat(formatter.formatField("INTEGER", "123")).isEqualTo("123"); | ||
| assertThat(formatter.formatField("NUMBER", "45.67")).isEqualTo("45.67"); |
There was a problem hiding this comment.
(sugg, b): for security, can we ensure that the numbers are number shaped before allowing them through?
| assertThat(monthResult.get(1)).isEqualTo("'2024-02-29'"); // Leap year check | ||
|
|
||
| // Test yyyy Range | ||
| List<String> yearRange = List.of("2023", "2023"); |
There was a problem hiding this comment.
| 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)) |
There was a problem hiding this comment.
can we also test too long?
| String whereFragment = | ||
| mockWhereClauseService.buildBasicWhereFragment(reportConfig, basicFilterRequest); | ||
|
|
||
| assertThat(whereFragment).isEqualTo("([ColumnName] IN (condition1))"); |
There was a problem hiding this comment.
| assertThat(whereFragment).isEqualTo("([ColumnName] IN (condition1))"); | |
| assertThat(whereFragment).isEqualTo("([ColumnName] IN ('condition1'))"); |
| mockWhereClauseService = new WhereClauseService(fieldFormatter); | ||
| // Default behavior for formatter: return value as is | ||
| Mockito.lenient() | ||
| .when(fieldFormatter.formatField(anyString(), anyString())) | ||
| .thenAnswer(ff -> ff.getArgument(1)); |
There was a problem hiding this comment.
(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?
There was a problem hiding this comment.
same with the field formatter - seems like those both could be static classes
| import java.util.List; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component |
There was a problem hiding this comment.
(q, nb): why have this be a component instead of just a regular utility class?
|



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()inReportSQLBuilder. This only implements the basic filters.Tickets
Checklist before requesting a review