How to add optional column in JDBI? - java

I have big SQL, about 50 lines, and we use JDBI with Postgresql, and I need to add one additional not mandatory column, so I did:
,case when :byPortfolio is true then ia1.investmentAccountAbbreviationCode else null end as portfolio
so if :byPortfolio is true this column should be removed, and if false this column should be present. Also ia1.investmentAccountAbbreviationCode should be present in Group by clause if it's present, so I added:
,case when :byPortfolio2 is true then ia1.investmentAccountAbbreviationCode else null end
and it works if I fun it as SQL query, then I copied it to sql.stg template file for JDBI and now when I run it I see an exceptioin:
org.postgresql.util.PSQLException: ERROR: column "ia1.investmentaccountabbreviationcode" must appear in the GROUP BY clause or be used in an aggregate function
I don't understand why JDBI can't handle it. Function described like this:
#SqlQuery
#UseStringTemplateSqlLocator
#RegisterBeanMapper(SolutionOwnershipDataEntity.class)
List<SolutionOwnershipDataEntity> getSolutionOwnershipPercentageOfFundByDate(
#Bind("inputDate") String inputDate,
#Bind("fundSeriesId") Integer fundSeriesId,
#Bind("governanceCommitteeTypeCode") String governanceCommitteeTypeCode,
#Bind("byPortfolio") boolean byPortfolio,
#Bind("assetClass") String assetClass,
#Bind("vehicleType") String vehicleType);
Please help to fix it? We can fix current code or maybe there is other way to handle unnecessary column? I mean I can write two 50 lines SQL with same code, only difference would be presence or absence of one column, but I want to have one query with one unnecessary column, what is the best way to achieve this?

For byPortfolio, you need to use #Define instead of #Bind.
#Bind is only for binding values to parameters of a query, while #Define can be used for placeholders and templating.
For the templating, you can then put the optional parts between <if(byPortfolio)> and <endif>. The template syntax is not related to normal SQL.
Here is a complete example from the documentation:
#SqlQuery("""
select id, name
from account
order by <if(sort)> <sortBy>, <endif> id
""")
#UseStringTemplateEngine
List<Account> selectAll(#Define boolean sort, #Define String sortBy);
For more details, please have look at the official documentation: https://jdbi.org/#_stringtemplate_4

Related

How to use DSL.coalesce with lists of fields?

Using Jooq, I am trying to fetch from a table by id first, if no matches found, then fetch by handle again.
And I want all fields of the returned rows, not just one.
Field<?> firstMatch = DSL.select(Tables.MY_TABLE.fields())
.from(Tables.MY_TABLE.fields())
.where(Tables.MY_TABLE.ID.eq(id))
.asfield(); // This is wrong, because it supports only one field, but above we selected Tables.MY_TABLE.fields(), which is plural.
Field<?> secondMatch = DSL.select(Tables.MY_TABLE.fields())
.from(Tables.MY_TABLE.fields())
.where(Tables.MY_TABLE.HANDLE.eq(handle))
.asfield(); // Same as above.
dslContext.select(DSL.coalesce(firstMatch, secondMatch))
.fetchInto(MyClass.class);
Due to the mistake mentioned above in the code, the following error occurs:
Can only use single-column ResultProviderQuery as a field
I am wondering how to make firstMatch and secondMatch two lists of fields, instead of two fields?
I tried
Field<?>[] secondMatch = DSL.select(Tables.MY_TABLE.fields())
.from(Tables.MY_TABLE.fields())
.where(Tables.MY_TABLE.HANDLE.eq(handle))
.fields();
but the following error occurred in the line containing DSL.coalesce
Type interface org.jooq.Field is not supported in dialect DEFAULT
Thanks in advance!
This sounds much more like something you'd do with a simple OR?
dslContext.selectFrom(MY_TABLE)
.where(MY_TABLE.ID.eq(id))
// The ne(id) part might not be required...
.or(MY_TABLE.ID.ne(id).and(MY_TABLE.HANDLE.eq(handle))
.fetchInto(MyClass.class);
If the two result sets should be completely exclusive, then you can do this:
dslContext.selectFrom(MY_TABLE)
.where(MY_TABLE.ID.eq(id))
.or(MY_TABLE.HANDLE.eq(handle).and(notExists(
selectFrom(MY_TABLE).where(MY_TABLE.ID.eq(id))
)))
.fetchInto(MyClass.class);
If on your database product, a query using OR doesn't perform well, you can write an equivalent query with UNION ALL, which might perform better.

Less repetition in jOOQ query

Any idea on how I could define the following jOOQ query with less repetition?
I am using jOOQ 3.11.4.
db.insertInto(ACCOUNT,
ACCOUNT.ACCOUNT_ID,
ACCOUNT.EMAIL,
ACCOUNT.FIRST_NAME,
ACCOUNT.LAST_NAME,
ACCOUNT.IS_ADMIN,
ACCOUNT.PASSWORD)
.values(account.accountId,
account.email,
account.firstName,
account.lastName,
account.isAdmin,
account.password)
.onConflict(ACCOUNT.ACCOUNT_ID)
.doUpdate()
.set(ACCOUNT.EMAIL, account.email)
.set(ACCOUNT.FIRST_NAME, account.firstName)
.set(ACCOUNT.LAST_NAME, account.lastName)
.set(ACCOUNT.IS_ADMIN, account.isAdmin)
.set(ACCOUNT.PASSWORD, account.password)
.returning(
ACCOUNT.ACCOUNT_ID,
ACCOUNT.EMAIL,
ACCOUNT.FIRST_NAME,
ACCOUNT.LAST_NAME,
ACCOUNT.IS_ADMIN,
ACCOUNT.PASSWORD
)
.fetchOne()
(I turns out my question is mostly code, and StackOverflow does not let me post it as is, without adding more details, which I do not think is necessary for my question, but nevertheless, they want me to post some more text, which I am doing right now by typing this message, and I hope you did not have to read to the end.)
Since you're passing all the columns to the insert statement, you might write this instead:
// Create an AccountRecord that contains your POJO data
Record rec = db.newRecord(ACCOUNT);
rec.from(account);
// Don't pass the columns to the insert statement explicitly
db.insertInto(ACCOUNT)
// But pass the record to the set method. It will use all the changed values
.set(rec)
// Use the MySQL syntax, which can be emulated on PostgreSQL using ON CONFLICT
.onDuplicateKeyUpdate()
// But pass the record to the set method again
.set(rec)
// Don't specify any columns to the returning clause. It will take all the ACCOUNT columns
.returning()
.fetchOne();

Liquibase preconditions: How do I check for a column being the correct data type?

I have a db upgrade script to change some datatypes on a few columns. I want to do a preCondition check, and call ALTER TABLE only when it is a DECIMAL datatype, but I will want it to be changed to INTEGER.
Couldn't find a predefined precondition for this, and could not write an sqlCheck either.
There's no built-in precondition for column's dataType in liquibase.
You may just check whether the column exists or not. If it's already of the datatype you need, no error will be thrown.
OR
You can use sqlCheck in your preconditions and it'll be something like this:
<preConditions onFail="MARK_RAN">
<not>
<sqlCheck expectedResult="DECIMAL">
SELECT DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'your_table_name'
AND COLUMN_NAME = 'your_column_name'
</sqlCheck>
</not>
</preConditions>
Another answer already mentions how to do a sqlcheck. However, the actual SQL for Teradata would be something different.
In Teradata you would use a query similar to the following and expect the columnType='D' for decimal values
Select ColumnType
From DBC.ColumnsV
Where databasename='yourdatabasename'
and tablename='yourtablename'
and columnname='yourcolumnname';
You could also do something like this if you want a more human readable column type instead of a type code:
Select Type(tablename.columnname);
I know the question was for Teradata, but principle is the same.
I prefer SQL files, so in changelog I have (for Oracle), is:
<include file="roles.sql" relativeToChangelogFile="true" />
and then in roles.sql
there is
--changeset betlista:2022-01-04_2200-87-insert
--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 select count(*) from ddh_audit.DDH_USER_ROLE where id = 87;
insert into ddh_audit.DDH_USER_ROLE(id, role_name, description)
values(87, 'CONTAINERS_READONLY', 'Can read Containers reference data');
the query added by David Cram would make the trick.
I do not know and I didn't try if condition could be on multiple lines, I know --rollback can.

How to prevent SQL injection when dealing with dynamic table/column names?

I am using jdbc PreparedStatement for data insertion.
Statement stmt = conn.prepareStatement(
"INESRT INTO" + tablename+ "("+columnString+") VALUES (?,?,?)");
tablename and columnString are something that is dynamically generated.
I've tried to parameterise tablename and columnString but they will just resolve to something like 'tablename' which will violate the syntax.
I've found somewhere online that suggest me to lookup the database to check for valid tablename/columnString, and cache it somewhere(a Hashset perhaps) for another query, but I'm looking for better performance/ quick hack that will solve the issue, perhaps a string validator/ regex that will do the trick.
Have anyone came across this issue and how do you solve it?
I am not a java-guy, so, only a theory.
You can either format dynamically added identifiers or white-list them.
Second option is way better. Because
most developers aren't familiar enough with identifiers to format them correctly. Say, to quote an identifier, which is offered in the first comment, won't make it protected at all.
there could be another attack vector, not entirely an injection, but similar: imagine there is a column in your table, an ordinary user isn't allowed to - say, called "admin". With dynamically built columnString using data coming from the client side, it's piece of cake to forge a privilege escalation.
Thus, to list all the possible (and allowed) variants in your code beforehand, and then to verify entered value against it, would be the best.
As of columnString - is consists of separate column names. Thus, to protect it, one have to verify each separate column name against a white list, and then assemble a final columnString from them.
Create a method that generates the sql string for you:
private static final String template = "insert into %s (%s) values (%s)";
private String buildStmt(String tblName, String ... colNames) {
StringJoiner colNamesJoiner = new StringJoiner(",");
StringJoiner paramsJoiner = new StringJoiner(",");
Arrays.stream(colNames).forEach(colName -> {
colNamesJoiner.add(colName);
paramsJoiner.add("?");
});
return String.format(template, tblName, colNamesJoiner.toString(), paramsJoiner.toString());
}
Then use it...
Statement stmt = conn.prepareStatement(buildStmt(tablename, [your column names]));
As an elaboration on #Anders' answer, don't use the input parameter as the name directly, but keep a properties file (or database table) that maps a set of allowed inputs to actual table names.
That way any invalid name will not lead to valid SQL (and can be caught before any SQL is generated) AND the actual names are never known outside the application, thus making it far harder to guess what would be valid SQL statements.
I think, the best approach is to get table and columns names from database or other non user input, and use parameters in prepared statement for the rest.
There are multiple solutions we can apply.
1) White List Input Validation
String tableName;
switch(PARAM):
case "Value1": tableName = "fooTable";
break;
case "Value2": tableName = "barTable";
break;
...
default : throw new InputValidationException("unexpected value provided for table name");
By doing this input validation on tableName, will allows only specified tables in the query, so it will prevents sql injection attack.
2) Bind your dynamic columnName(s) or tableName(s) with special characters as shown below
eg:
For Mysql : use back codes (`)
Select `columnName ` from `tableName `;
For MSSQL : Use double codes(" or [ ] )
select "columnName" from "tableName"; or
select [columnName] from [tableName];
Note: Before doing this you should sanitize your data with this special characters ( `, " , [ , ] )

jpql left join fetch not returning results for like

In a spring mvc app using hibernate and MySQL, I have written the following query method to return a list of names with patients:
#SuppressWarnings("unchecked")
public Collection<Person> findPersonByLastName(String ln) throws DataAccessException{
Query query = this.em.createQuery("SELECT DISTINCT pers FROM rimPerson pers left join fetch pers.names nm WHERE nm.family LIKE :lnm");
query.setParameter("lnm", ln);
return query.getResultList();
}
This is producing the following hibernate sql:
Hibernate:
select distinct
person0_.hppid as hppid1_340_0_,
names1_.HJID as HJID1_89_1_,
person0_2_.classCode_HJID as classCod2_339_0_,
person0_1_.administrativeGenderCode_HJID as administ2_341_0_,
person0_1_.birthTime_HJID as birthTim3_341_0_,
names1_.DELIMITER_ as DELIMITE2_89_1_,
names1_.FAMILY as FAMILY3_89_1_,
names1_.named_entity_hppid as named5_89_1_,
names1_.SUFFIX as SUFFIX4_89_1_,
names1_.name_entity_HJID as name9_340_0__,
names1_.HJID as HJID1_89_0__
from
rim_person person0_ inner join rim_living_subject person0_1_ on person0_.hppid=person0_1_.hppid
inner join rim_entity person0_2_ on person0_.hppid=person0_2_.hppid
inner join rim_infrastructure_root person0_3_ on person0_.hppid=person0_3_.hppid
left outer join EN names1_ on person0_.hppid=names1_.name_entity_HJID
where names1_.FAMILY like ?
When I call the above jpql method with the following command, it returns zero results:
this.myappService.findPersonByLastName("");
I also get zero results when I cut and past the above generated hibernate code into the MySQL command line client and replace ? with ''.
If, however, I remove the where names1_.FAMILY like ? from the hibernate generated sql above and place the shortened sql into the MySQL command line client, I get four results, eachof which has a value for the lastname field.
How can I change the jpql so that it generates a hibernate query that returns the four results when `` is passed as the empty string parameter? I want the result set to include every result when the user gives empty input, but to give filtered results when the user types in any given text input.
The typical reason that like fails to do what you think it ought to do is to forget to put a wildcard in the pattern string. For example, if you want to match all user names that begin with 'Code' you must do something like name like 'Code%', NOT name like 'Code'. You can control exactly what your predicate matches with careful placement of %s in your string.
Try this to see all entities no matter what the value in family:
this.myappService.findPersonByLastName("%");
It is kinda cheesy to have the caller of findPersionByLastName have to put in the % wildcard. A better implementation is to have the caller specify which last name they are looking for, and then have the code that constructs the query put the wildcard in the right place. When you are looking for last names, you might do something like this:
query.setParameter("lnm", "%" + ln);
That would match anything that ends with the parameter that is passed to the method.

Categories