Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fineract-1910 data table query advanced #3321

Conversation

marta-jankovics
Copy link
Contributor

Description

Describe the changes made and why they were made.

Ignore if these details are present on the associated Apache Fineract JIRA ticket.

Checklist

Please make sure these boxes are checked before submitting your pull request - thanks!

  • Write the commit message as per https://github.com/apache/fineract/#pull-requests

  • Acknowledge that we will not review PRs that are not passing the build ("green") - it is your responsibility to get a proposed PR to pass the build, not primarily the project's maintainers.

  • Create/update unit or integration tests for verifying the changes made.

  • Follow coding conventions at https://cwiki.apache.org/confluence/display/FINERACT/Coding+Conventions.

  • Add required Swagger annotation and update API documentation at fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm with details of any API changes

  • Submission is not a "code dump". (Large changes can be made "in repository" via a branch. Ask on the developer mailing list for guidance, if required.)

FYI our guidelines for code reviews are at https://cwiki.apache.org/confluence/display/FINERACT/Code+Review+Guide.

@marta-jankovics
Copy link
Contributor Author

marta-jankovics commented Jul 21, 2023

Review only, no tests yet, to be added - tests added

@galovics
Copy link
Contributor

Squash your commits pls and format the PR title properly (FINERACT-1234 Something)

@galovics
Copy link
Contributor

galovics commented Jul 21, 2023

@marta-jankovics please change back the javadoc/comments where you've only changed formatting. It's polluting the PR and makes it hard to review.

Also, do we need a 4k line change in this specific PR, just to extend the datatable functionality? Let's make the changes as small as possible unless we have a good reason to change it in its core.

Until that, it's way too big to review.

@marta-jankovics marta-jankovics changed the title Fineract 1910 data table query advanced marta Fineract-1910 data table query advanced marta Jul 21, 2023
@marta-jankovics
Copy link
Contributor Author

marta-jankovics commented Jul 21, 2023

Sorry I did not know that merge the PRs can not be done with squash and merge here. I will then squash the commits (most of them are merge).
'just to extend the datatable functionality' it is a questionable statement, maybe better to ask first.
Anyway I see some merge commits are represented as changed files which should not be, I'll check.

About the formatting: I did not change anything, spotless did. I reverted 300+ changes after spotless run but I do not check it again. Would be nice to create a check-style which does not touch the existing code (and does not ruin the formatted enums, does not add empty line after each method start, allows 200 chars line length...)

Copy link
Contributor

@galovics galovics left a comment

Choose a reason for hiding this comment

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

There are a few things still to do. Let's make the PR less complex, less polluted with unnecessary changes.

Also, I see zero tests added yet there are thousands of line changes in the codebase. Please cover those.

Thanks.

return escape(databaseTypeResolver.databaseType(), arg);
}

public static String escape(DatabaseType dialect, String arg) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need to pass the dialect here but not everywhere else?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

because it is static

Copy link
Contributor

Choose a reason for hiding this comment

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

This class is a spring bean, let's not make static methods on it but use it as a proper component.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I do not agree with your opinion.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd also vote to remove the "static"... we have a lot of "utility" classes that have static methods and are just creating problems down the road (example: just have a look at how a tenant's timezone is resolved). Globally there is also no added value in terms of saving mem or similar (by not instantiating a full object); effectively "@component"s are singletons... and in that form they are composable... with static they are not... there are plenty of examples where this leads to bloated static functions (more than 500 lines of code). The fix is very easy, the class is already instantiated as a singleton, so there's added value by using the static keyword.

Copy link
Contributor

Choose a reason for hiding this comment

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

... and on top of that: if we don't declare static then we don't have to pass the dialect parameter... and if we don't need to pass that parameter then we don't need to declare a new function which makes this class more explicit and leaves less to guess for the dev.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The dialect parameter might not be used for the currently supported dialects but will be needed for the next supported one, that is why this class has a reference to the dialect.
You can not instantiate a component, only from a component (mainly) so if not static then a parameter of the SqlGenerator component for every method in the call chain. That would be another solution to this, right.

@@ -189,4 +207,49 @@ public String castJson(String sql) {
throw new IllegalStateException("Database type is not supported for casting to json " + databaseTypeResolver.databaseType());
}
}

public static String alias(@NotNull String field, String alias) {
Copy link
Contributor

Choose a reason for hiding this comment

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

How are these methods below specific to the database type?

Copy link
Contributor

Choose a reason for hiding this comment

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

Just looking at all these functions in general (not only Marta's): this is getting more and more complex... a fight we cannot win.... and in the end we have compatibility for 2 database systems. A lot of work (also future maintenance). Why not hit the breaks here and use e. g. JOOQ. Slowly, but steadily we are recreating it here with less features and less supported databases. For another day to decide, but I think it will be an important improvement.... and all this code can go away.

return formatValue(databaseTypeResolver.databaseType(), columnType, value);
}

public static String formatValue(DatabaseType dialect, JdbcJavaType columnType, String value) {
Copy link
Contributor

Choose a reason for hiding this comment

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

How is this specific again to a dialect?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do not understand this question, of course formatting a value is dialect dependent.

Copy link
Contributor

Choose a reason for hiding this comment

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

I dont see that, the parameter is not even used.

Copy link
Contributor

Choose a reason for hiding this comment

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

Same arguments here, see my previous comments "escape" function.

@@ -58,10 +58,10 @@ public DatabaseType databaseType() {
}

public boolean isPostgreSQL() {
return DatabaseType.POSTGRESQL.equals(currentDatabaseType.get());
return DatabaseType.POSTGRESQL == currentDatabaseType.get();
Copy link
Contributor

Choose a reason for hiding this comment

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

Why was this necessary? It's not an issue, I'm just curious.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would dump these functions too (see my previous comment on DatabaseType). What are we saving here? Again, let's say we add support for 10 more databases...

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess my point is: you can iterate over all values of an enum or use it for example in a switch statement... those "is..." functions you can only use in (ugly) if-then-else-if statements.... I think this style has a tendency to bloat code...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These isX functions are not new and sometimes really useful, when you do not want to know which enum(s) belong to a category.
For identity please see my previous comment.

}

public void validateValues(String... values) {
if (values == null ? paramCount != 0 : (paramCount < 0 ? values.length < -paramCount : values.length != paramCount)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This condition is hard to understand unfortunately, can't we do it differently?

* if(loan.isSyncDisbursementWithMeeting()){ // validate actual disbursement date against meeting date
* CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByLoanId
* (loan.getId(), CalendarEntityType.LOANS.getValue()); this.loanEventApiJsonValidator
* if(loan.isSyncDisbursementWithMeeting()){ // validate actual disbursement date against meeting date CalendarInstance calendarInstance =
Copy link
Contributor

Choose a reason for hiding this comment

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

Not needed. Please revert.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Spotless

@@ -48,8 +48,7 @@
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;

/**
* Abstract implementation of {@link LoanRepaymentScheduleTransactionProcessor} which is more convenient for concrete
* implementations to extend.
* Abstract implementation of {@link LoanRepaymentScheduleTransactionProcessor} which is more convenient for concrete implementations to extend.
Copy link
Contributor

Choose a reason for hiding this comment

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

Not needed. Please revert.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

spotless

PagedRequest<AdvancedQueryRequest> queryRequest, @Context final UriInfo uriInfo) {
final org.springframework.data.domain.Page<JsonObject> result = transactionsSearchServiceImpl.queryAdvanced(savingsId,
queryRequest);
return this.toApiJsonSerializer.serializePretty(ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()), result);
Copy link
Contributor

Choose a reason for hiding this comment

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

As I said for the datatables API, you can go with Page<...> since this is a new API.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I do not know where you said this already, but this is a v1 API, and I learned that only v2 APIs can go with Object response. Additionally the response here is dynamic.

Copy link
Contributor

Choose a reason for hiding this comment

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

The rule is

for V2 it is mandatory to be object response
for V1 new API it is okay to be object response

@@ -43,7 +43,7 @@ public static class Filters {
@Data
public static class RangeFilter<T> {

private RangeOperator operator;
private SqlOperator operator;
Copy link
Contributor

Choose a reason for hiding this comment

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

Now this I don't like. We're exposing "SqlOperators" to higher layers (i.e.API) which shouldn't happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like this. Why can't sql operators be exposed to higher levels??? (Actually it was exposed before but as simple string or RangeOparator)
I was thinking on even using graphQL, but this was not complex enough for that.

import org.apache.fineract.portfolio.search.data.FilterData;
import org.springframework.jdbc.support.rowset.SqlRowSet;

public class SearchUtil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Woah, this class is really really complex and impossible to review. Can we separate pieces out according to responsibilities?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you mean? A util class with 10 static methods, all search related is not complex.

@marta-jankovics marta-jankovics force-pushed the FINERACT-1910_Data_Table_query_Advanced_marta branch 3 times, most recently from 780304c to b233f34 Compare July 31, 2023 15:13
@marta-jankovics marta-jankovics force-pushed the FINERACT-1910_Data_Table_query_Advanced_marta branch from b233f34 to 68aee23 Compare July 31, 2023 21:32
@adamsaghy adamsaghy requested review from vidakovic and removed request for vidakovic August 1, 2023 09:00
Copy link
Contributor Author

@marta-jankovics marta-jankovics left a comment

Choose a reason for hiding this comment

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

@vidakovic Could you please review org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator
SQL_PATTERN extended by these characters: {,},[,],+
We should support json value in dynamic insert statements.
Please confirm that it is ok from security perspective.

Tried to use your solution from
https://github.com/apache/fineract/blame/1.8.4/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/utils/SQLInjectionValidator.java
and introduced INJECTION_PATTERNS as black patterns, but ReadWriteNonCoreDataServiceImplTest failed, as "1; DROP TABLE m_loan; SELECT" in a select query was not detected as injection.
Added a TODO FINERACT-1870 so you will add the final solution.
Thank you

@marta-jankovics marta-jankovics force-pushed the FINERACT-1910_Data_Table_query_Advanced_marta branch from 68aee23 to 9d2f3fe Compare August 1, 2023 11:13
@vidakovic vidakovic requested review from vidakovic, galovics and adamsaghy and removed request for galovics and adamsaghy August 1, 2023 19:39
POSTGRESQL, //
;

public boolean isMySql() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any added value of having these "convenience functions"? I think they hide more than they actually help... what happens if we add support for 10 more databases? We have other examples in the code base where these things quickly went out of hand and people started adding all kind of functionality to enums... whereas these files should be really simple and readable. My 2 cents.
Is a simple MYSQL.equals(dbType) so much worse (for me it's more readable... imagine static imports of those enums)?
BTW: we should really use "equals" instead "==".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This enum is not new.
For enums the == comparison is better because

  • enums are singleton classes and it never happens that the same enum is not identical with itself
  • implementation of equals for enums is actually ==
  • == is null safe
  • == shows what it is, does not give the impression as if it could be not t
    In my opinion enum is better than static string because
  • it can have its own functionality
  • type safe and value safe
  • singleton and identical
  • ability to use as list type
  • serialised by name (not by value)

@marta-jankovics marta-jankovics force-pushed the FINERACT-1910_Data_Table_query_Advanced_marta branch 6 times, most recently from 8b64d4c to 7d9c901 Compare August 5, 2023 22:05
@marta-jankovics marta-jankovics force-pushed the FINERACT-1910_Data_Table_query_Advanced_marta branch from 7d9c901 to 6cde7ad Compare August 6, 2023 00:00
@marta-jankovics marta-jankovics force-pushed the FINERACT-1910_Data_Table_query_Advanced_marta branch from 6cde7ad to cbdb729 Compare August 14, 2023 10:57
@marta-jankovics marta-jankovics force-pushed the FINERACT-1910_Data_Table_query_Advanced_marta branch 7 times, most recently from e3861ee to 570e380 Compare August 26, 2023 22:50
@marta-jankovics marta-jankovics changed the title Fineract-1910 data table query advanced marta Fineract-1910 data table query advanced Aug 28, 2023
@@ -86,4 +89,106 @@ public static LocalDate getBusinessLocalDate() {
public static long getDifferenceInDays(final LocalDate localDateBefore, final LocalDate localDateAfter) {
return DAYS.between(localDateBefore, localDateAfter);
}

public static int compareToBusinessDate(LocalDate date) {
Copy link
Contributor

Choose a reason for hiding this comment

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

unused

return compare(date, getBusinessLocalDate());
}

public static boolean isEqualBusinessDate(LocalDate date) {
Copy link
Contributor

Choose a reason for hiding this comment

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

unused

return isEqual(date, getBusinessLocalDate());
}

public static boolean isBeforeBusinessDate(LocalDate date) {
Copy link
Contributor

Choose a reason for hiding this comment

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

unused

return isBefore(date, getBusinessLocalDate());
}

public static boolean isAfterBusinessDate(LocalDate date) {
Copy link
Contributor

Choose a reason for hiding this comment

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

unused

return isAfter(date, getBusinessLocalDate());
}

public static int compare(LocalDate first, LocalDate second) {
Copy link
Contributor

Choose a reason for hiding this comment

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

unused, also it is wrong: if we are comparing two null LocalDate it will says the first is earlier which is not true.

@@ -1940,8 +2047,8 @@ private String validateDatatableRegistered(String datatable) {

private boolean isRegisteredDataTable(final String datatable) {
final String sql = "SELECT COUNT(application_table_name) FROM " + TABLE_REGISTERED_TABLE + " WHERE registered_table_name = ?";
final int count = jdbcTemplate.queryForObject(sql, Integer.class, datatable);
return count > 0;
final Integer count = jdbcTemplate.queryForObject(sql, Integer.class, datatable);
Copy link
Contributor

Choose a reason for hiding this comment

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

count function can never return null. using int is more than fine and no need for null check

Copy link
Contributor Author

Choose a reason for hiding this comment

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

for me it was a checkstyle warning, if it can not return null, it is fine, but lets be safe

@@ -1890,7 +1997,7 @@ public boolean isDatatableAttachedToEntityDatatableCheck(final String datatableN
+ " JOIN m_entity_datatable_check edc ON edc.x_registered_table_name = xrt.registered_table_name"
+ " WHERE edc.x_registered_table_name = '" + datatableName + "'";
final Long count = this.jdbcTemplate.queryForObject(sql, Long.class);
return count > 0;
return count != null && count > 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

count function can never return null. using int is more than fine and no need for null check

Copy link
Contributor Author

Choose a reason for hiding this comment

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

for me it was a checkstyle warning, if it can not return null, it is fine, but lets be safe

PagedRequest<AdvancedQueryRequest> queryRequest, @Context final UriInfo uriInfo) {
final org.springframework.data.domain.Page<JsonObject> result = transactionsSearchServiceImpl.queryAdvanced(savingsId,
queryRequest);
return this.toApiJsonSerializer.serializePretty(ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()), result);
Copy link
Contributor

Choose a reason for hiding this comment

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

The rule is

for V2 it is mandatory to be object response
for V1 new API it is okay to be object response

public static List<String> validateToJdbcColumnNames(List<String> columns, Map<String, ResultsetColumnHeaderData> headersByName,
boolean allowEmpty) {
List<ResultsetColumnHeaderData> columnHeaders = validateToJdbcColumns(columns, headersByName, allowEmpty);
return columnHeaders.stream().map(e -> e == null ? null : e.getColumnName()).toList();
Copy link
Contributor

Choose a reason for hiding this comment

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

in what situation can the columnHeaders element be null?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

columnHeaders can not be null, but the elements in it could be (in case there are null values in the columns list and allowEmpty is true)

assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(8));
assertNull(((List) ((Map) ((List) items.get("data")).get(1)).get("row")).get(9));
List headers = (List) items.get("columnHeaders");
List values0 = (List) ((Map) ((List) items.get("data")).get(0)).get("row");
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please rename this to values?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no, because its the first table row values, and there are also values from the second table row, see line 525

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. FirstRowValues, secondRowValues then?

@marta-jankovics marta-jankovics force-pushed the FINERACT-1910_Data_Table_query_Advanced_marta branch 3 times, most recently from 92d0f31 to e59b580 Compare August 28, 2023 12:00
@marta-jankovics marta-jankovics force-pushed the FINERACT-1910_Data_Table_query_Advanced_marta branch from e59b580 to 2b561dd Compare August 28, 2023 12:15
@adamsaghy adamsaghy merged commit f1e7011 into apache:develop Aug 28, 2023
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants