-
Notifications
You must be signed in to change notification settings - Fork 820
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
Named placeholders #1946
base: master
Are you sure you want to change the base?
Named placeholders #1946
Conversation
JDBC doesn't support named parameters. I suspect that things like select * from foo where name like ':foo' might fail? |
PreparedStatement doesn't, but CallableStatement does. I have no idea why that is so. I have added your test case to org.postgresql.core.ParserTest.namedPlaceholderSimple. I already had that case covered, but put it in anyway. |
I see that org.postgresql.replication.ReplicationConnectionTest fails I don't that is related to this PR? |
looks good to me. There are some format only changes that I would prefer to see removed |
I went through the changes, and have undone those changes. I also undid a few remnants of some exploratory surgery... |
Everything but org.postgresql.replication.ReplicationConnectionTest passes now. |
Don't worry about that one. |
Ok, I won't then. Anything else that needs taking care of? |
I'd like to see others review it as well. @vlsi ? |
import java.util.Map; | ||
import java.util.TimeZone; | ||
import java.util.UUID; | ||
|
||
class PgPreparedStatement extends PgStatement implements PreparedStatement { | ||
class PgPreparedStatement extends PgStatement implements PGPreparedStatement { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realized that having PGPreparedStatement extend PreparedStatement makes life easier as a user.
I don't have to handle both a PGPreparedStatement and PreparedStatement variable to get access to both standard and PG-specific methods.
|
||
@Param({"false", "true"}) | ||
public boolean named; | ||
@Param({"0", "1", "10", "20", "100", "1000"}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using named placeholders doesn't seem to be hurting performance, at least not at 1000 placeholders.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you mean "old code" performs exactly the same as the new one?
Could you please show the results?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be happy to, as soon as the feature set stabilizes.
@bokken are you good with this now ? |
I like the feature of being able to bind multiple parameters to the same value though I'm not too fond of supporting non-standard features in the core driver. Main worry being we end up with a conflict with core as to what's valid SQL. (Would be nice to be able to use $1, $2, etc, with the regular positional binding...). If we're going to add this let's make sure to document exactly what's a valid format for picking out parameters. From skimming the PR I think it's any identifier char following a Does this always try to parse for named parameters? Wondering if there's a situation where existing code would break as a result of parsing out the new format. Also, if we're always parsing the user SQL for this pattern, any thoughts on supporting $1, $2, ... etc in a similar way (rather than just |
When to parse, (and what the aim is) seems unclear in places.
@vlsi test are back in the green. Does anything else need sorting out?
Compared to this branch (2a36984)
That looks ok to me. |
@vlsi @davecramer Will this be going anywhere? If not, I might as well close the PR. |
@kimjand , thanks for pushing this forward. I've reviewed the code a bit (see Some notes:
|
PreparedStatement prepareStatement(String sql, PlaceholderStyle placeholderStyle) throws SQLException; | ||
|
||
PreparedStatement prepareStatement(String sql, int autoGeneratedKeys, PlaceholderStyle placeholderStyle) throws SQLException; | ||
|
||
PreparedStatement prepareStatement(String sql, int @Nullable [] columnIndexes, PlaceholderStyle placeholderStyle) throws SQLException; | ||
|
||
PreparedStatement prepareStatement(String sql, String @Nullable[] columnNames, PlaceholderStyle placeholderStyle) throws SQLException; | ||
|
||
PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, PlaceholderStyle placeholderStyle) throws SQLException; | ||
|
||
PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability, PlaceholderStyle placeholderStyle) throws SQLException; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want duplicating all the prepare methods? Is it a shortcut for setPlacehoderStyle(...); prepareStatement(...)
?
If that is a shortcut, I would avoid adding new prepareStatement, otherwise we could get quite a few of them quite fast
"Specifies which extra non-standard styles of placeholders are to be recognized by the driver, if any. None will disable parameters in Prepared Statements", | ||
false, | ||
new String [] {"any", "jdbc", "named", "native", "none"}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would be the use case for having placeholderStyle=none
?
I think it makes the usage confusing
if (placeholderNameToNativeParameterIndex == null) { | ||
placeholderNameToNativeParameterIndex = new HashMap<>(placeholderCount()); | ||
for (int i = 0; i < placeholderCount(); i++) { | ||
Nullness.castNonNull(placeholderNameToNativeParameterIndex) | ||
.put(NativeQuery.bindName(i + 1), i); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we exclude $
from the name of the native parameter, then this code could be like Integer.parseInt(placeholderName) + if (result > placeholderCount()) return null
.
An alternative option could be a static map within NativeQuery
+ if (result > placeholderCount()) return null
PGPreparedStatement ps = | ||
conn.prepareStatement("SELECT col_a FROM mytable WHERE col_a < $1 AND col_b > $1") | ||
.unwrap(PGPreparedStatement.class); | ||
|
||
ps.setInt("$1", 42); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest having "1"
here for consistency with named
parameters
} else { | ||
fragmentStart = i; | ||
} | ||
} else { | ||
i = end; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In most cases, placing the shortest if
branch first makes the code easier to follow.
WITH(false); | ||
|
||
final String lowerName; | ||
static final Map<Integer, List<SqlCommandType>> sqlCommandTypeLookup = new HashMap<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static final Map<Integer, List<SqlCommandType>> sqlCommandTypeLookup = new HashMap<>(); | |
static final Map<Integer, List<SqlCommandType>> SQL_COMMAND_TYPE_LOOKUP = new HashMap<>(); |
@@ -529,6 +530,173 @@ public void clearParameters() throws SQLException { | |||
preparedParameters.clear(); | |||
} | |||
|
|||
@Override | |||
public void setObject(String parameterName, Object x, int targetSqlType) throws SQLException { | |||
this.setObject(preparedParameters.getIndex(parameterName), x, targetSqlType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.setObject(preparedParameters.getIndex(parameterName), x, targetSqlType); | |
setObject(preparedParameters.getIndex(parameterName), x, targetSqlType); |
super.setUp(); | ||
PGProperty.REWRITE_BATCHED_INSERTS.set(props, true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setUp
opens a connection, so adding REWRITE_BATCHED_INSERTS
after setUp
is a bit too late
super.setUp(); | |
PGProperty.REWRITE_BATCHED_INSERTS.set(props, true); | |
PGProperty.REWRITE_BATCHED_INSERTS.set(props, true); | |
super.setUp(); |
Here's a reason why WIth That results in ambiguity for a SQL like |
On 12-05-2024 12:09, Vladimir Sitnikov wrote:
Here's a reason why |PlaceholderStyle.ANY| can't be implemented.
WIth |JDBC| style, we use |?| for a placeholder, and the user should use
|??| if they want a plain |?| in their SQL.
With |NATIVE| style, the query should be just a native SQL, so it could
use |?| (e.g. JSON operator).
With |NAMED| style, the placeholders would be |:name|, and I believe |?|
should be allowed as a standalone char.
That results in ambiguity for a SQL like |SELECT '{"a":1, "b":2}'::jsonb
? 'b'|. The driver won't be able to tell if |?| means a |JDBC|
placeholder or if it is just a plain |?|.
Yes, if we lift the restriction on escaping ? for non-JDBC prepared
statements then ANY won't work.
Removing the requirement for anything else than then JDBC-case may not
be a big problem, since we don't require escaping for non-prepared
statements, but I suppose it could be a source of confusion.
I don't mind removing ANY.
I will go through your other comments, and attempt a rebase.
Thanks!
|
Ideally, we would like to have zero parsing for |
@Override | ||
public boolean hasParameterNames() { | ||
return preparedParameters.hasParameterNames(); | ||
} | ||
|
||
@Override | ||
public List<String> getParameterNames() throws SQLException { | ||
/* We need to consult Query.getPlaceholderStyle() here, since Query.createParameterList may have | ||
returned a reference to ParameterContext.EMPTY_CONTEXT if no parameters were found. | ||
*/ | ||
return preparedParameters.getParameterNames(this.preparedQuery.query.getPlaceholderStyle()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This belongs to ParameterMetaData
On 12-05-2024 12:58, Vladimir Sitnikov wrote:
Ideally, we would like to have zero parsing for |NATIVE| except
something like |reWriteBatchedInserts|. However, I strongly believe with
|NATIVE| mode we should not replace JDBC stuff like |{fn ...}|.
That means "parameter context" for |NATIVE| is not very useful as the
driver should just accept whatever parameter user supplies.
I think that makes sense, if you go with the NATIVE-setting, you get
what is basically a pass-through behavior.
So we could just bounce the statement off of the back-end, and use
describe to get the metadata. That is, if we accept that we can no
longer use SIMPLE-mode, since we don't know anything about the statement
at hand...
Looking through the code, it seems to me that we need to be able perform
a describe when either getMetaData() is called OR the first
setXXX-method is being called. Only then do we know what to put into
ParameterList.
This changes a lot of things around. I think there are some limitations
to this approach that needs to be agreed upon before going any further.
Also I guess this was not really what I set out to do with this PR. I
would be more inclined to remove all the NATIVE-related stuff from here,
and then make that a separate project for later. What do you think?
|
What we learnt so far is:
As always, the smaller the change, the easier it is to review. For instance, you've moved keyword parsing logic from |
I currently do not have the time to work on this. Perhaps I will pick it up again at a later date. |
All Submissions:
New Feature Submissions:
./gradlew autostyleCheck checkstyleAll
pass ?This PR adds new methods for binding parameter values to named placeholders. The new methods are specified in
PGPreparedStatement implemented in PgPreparedStatement. This allows the following usage:
String sql = "select :aa||:aa||:a AS teststr";
PgPreparedStatement ps = con.prepareStatement(sql).unwrap(PgPreparedStatement.class);
ps.setString("a", "1");
ps.setString("aa", "2");
Since we can reuse the parameter value specified for each placeholder, we only need to send the value for :aa to the back-end once. This might even enable the query planner to do some optimizations, but I am uncertain of that.
I also provided a method that returns a list of placeholder names so it is possible to bind the parameter values through the existing index based methods.
In order to make this work I have made changes to Parser.java enabling the capture of names starting with colon. I moved the state concerning placeholders to a new class ParameterContext, in an effort to avoid a few array<->list conversions and centralize the logic concerning information about placeholders, their position in the SQL text and the required back-end parameters.
I think this is a worthwhile addition, since it dispenses with the need for things like Spring NamedParameterJdbcTemplate for those that want to used named placeholders, enabling parameter reuse.
All tests are green, but I would like some input on where more test coverage is needed.