Skip to content

Commit

Permalink
Support repeating named parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael-A-McMahon committed Mar 16, 2021
1 parent b5ed8b6 commit 409b6ff
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 25 deletions.
67 changes: 42 additions & 25 deletions src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ final class OracleStatementImpl implements Statement {
*/
@Override
public Statement bind(int index, Object value) {
requireNonNull(value, "value must not be null");
requireNonNull(value, "value is null");
requireValidIndex(index);
bindValues[index] = convertToJdbcBindValue(value);
return this;
Expand All @@ -249,11 +249,13 @@ public Statement bind(int index, Object value) {
* syntax.
* </p><p>
* If the specified {@code identifier} matches more than one parameter name,
* this method binds the {@code value} to the first matching parameter that
* appears when the SQL command is read from left to right. (Note: It is
* not recommended to use duplicate parameter names. Use
* {@link #bind(int, Object)} to set a value for a duplicate parameter name
* at a given index).
* then this method binds the {@code value} to all parameters having a
* matching name. For instance, when {@code 9} is bound to the parameter
* named "x", the following SQL would return all names having a birthday on
* the 9th day of the 9th month:
* <pre>
* SELECT name FROM birthday WHERE month=:x AND day=:x
* </pre>
* </p>
* @throws IllegalArgumentException {@inheritDoc}
* @throws IllegalArgumentException If the {@code identifier} does match a
Expand All @@ -264,9 +266,9 @@ public Statement bind(int index, Object value) {
*/
@Override
public Statement bind(String identifier, Object value) {
requireNonNull(identifier, "identifier must not be null");
requireNonNull(value, "value must not be null");
bindValues[indexOfIdentifier(identifier)] = convertToJdbcBindValue(value);
requireNonNull(identifier, "identifier is null");
requireNonNull(value, "value is null");
bindNamedParameter(identifier, value);
return this;
}

Expand All @@ -281,7 +283,7 @@ public Statement bind(String identifier, Object value) {
*/
@Override
public Statement bindNull(int index, Class<?> type) {
requireNonNull(type, "class type must not be null");
requireNonNull(type, "type is null");
requireValidIndex(index);
bindValues[index] = null;
return this;
Expand Down Expand Up @@ -310,16 +312,26 @@ public Statement bindNull(int index, Class<?> type) {
* duplicate parameter names. Use {@link #bindNull(int, Class)} to set the
* SQL {@code NULL} value for a duplicate parameter name at a given index).
* </p>
* </p><p>
* If the specified {@code identifier} matches more than one parameter name,
* then this method binds the SQL {@code NULL} value to all parameters
* having a matching name. For instance, when {@code NULL} is bound to the
* parameter named "x", the following SQL would return all names having a
* birthday with {@code NULL} day and month values:
* <pre>
* SELECT name FROM birthday WHERE month=:x AND day=:x
* </pre>
* </p>
* @throws IllegalArgumentException {@inheritDoc}
* @throws IllegalArgumentException If the {@code identifier} does match a
* case sensitive parameter name that appears in this {@code Statement's}
* SQL command.
*/
@Override
public Statement bindNull(String identifier, Class<?> type) {
requireNonNull(identifier, "identifier must not be null");
requireNonNull(type, "class type must not be null");
bindValues[indexOfIdentifier(identifier)] = null;
requireNonNull(identifier, "identifier is null");
requireNonNull(type, "type is null");
bindNamedParameter(identifier, null);
return this;
}

Expand Down Expand Up @@ -694,21 +706,26 @@ else if (index >= parameterNames.size()) {
}

/**
* Returns the 0-based index of a named parameter matching the specified
* {@code identifier}. The match is case-sensitive.
* @param identifier A parameter identifier
* @return The 0-based parameter index of the {@code identifier}
* Binds a {@code value} to all named parameters matching the specified
* {@code name}. The match is case-sensitive.
* @param name A parameter name. Not null.
* @param value A value to bind. May be null.
* @throws IllegalArgumentException if no named parameter matches the
* {@code identifier}
*/
private int indexOfIdentifier(String identifier) {
int index = parameterNames.indexOf(identifier);
if (index == -1) {
throw new IllegalArgumentException(
"Unrecognized parameter identifier: " + identifier);
private void bindNamedParameter(String name, Object value) {
boolean isMatched = false;

for (int i = 0; i < parameterNames.size(); i++) {
if (name.equals(parameterNames.get(i))) {
isMatched = true;
bindValues[i] = convertToJdbcBindValue(value);
}
}
else {
return index;

if (! isMatched) {
throw new IllegalArgumentException(
"Unrecognized parameter identifier: " + name);
}
}

Expand All @@ -730,7 +747,7 @@ private int indexOfIdentifier(String identifier) {
*/
private Object convertToJdbcBindValue(Object bindValue) {
if (bindValue == null) {
return bindValue;
return null;
}
else if (bindValue instanceof io.r2dbc.spi.Blob) {
return convertBlobBind((io.r2dbc.spi.Blob) bindValue);
Expand Down
66 changes: 66 additions & 0 deletions src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ class UnsupportedType {
asList(row.get(0, Integer.class), row.get(1,Integer.class)),
connection.createStatement(
"SELECT x, y FROM testBindByIndex WHERE x = 3 ORDER BY y"));

}
finally {
awaitExecution(connection.createStatement(
Expand Down Expand Up @@ -311,6 +312,43 @@ class UnsupportedType {
asList(row.get(0, Integer.class), row.get(1,Integer.class)),
connection.createStatement(
"SELECT x, y FROM testBindByName WHERE x = 3 ORDER BY y"));

// When the same name is used for multiple parameters, expect a value
// bound to that name to be set as the value for all of those parameters.
// Expect a value bound to the index of one of those parameters to be
// set only for the parameter at that index.
awaitUpdate(asList(1, 1, 1),
connection
.createStatement("INSERT INTO testBindByName VALUES (:same, :same)")
.bind("same", 4).add()
.bind("same", 4).bind(1, 5).add()
.bind(0, 4).bind(1, 6));
awaitQuery(asList(asList(4,4)),
row ->
asList(row.get(0, Integer.class), row.get(1,Integer.class)),
connection.createStatement(
"SELECT x, y FROM testBindByName WHERE x = :x_and_y AND y = :x_and_y")
.bind("x_and_y", 4));
awaitQuery(
asList(asList(4, 5), asList(4, 6)),
row ->
asList(row.get(0, Integer.class), row.get(1,Integer.class)),
connection.createStatement(
"SELECT x, y FROM testBindByName" +
" WHERE x = :both AND y <> :both" +
" ORDER BY y")
.bind("both", 4));
awaitQuery(asList(asList(4,4)),
row ->
asList(row.get(0, Integer.class), row.get(1,Integer.class)),
connection.createStatement(
"SELECT x, y FROM testBindByName" +
" WHERE x = :x_and_y" +
" AND (x * y) = :x_times_y" +
" AND y = :x_and_y")
.bind("x_times_y", 16)
.bind("x_and_y", 4));

}
finally {
awaitExecution(connection.createStatement("DROP TABLE testBindByName"));
Expand Down Expand Up @@ -617,6 +655,34 @@ public void testBindNullByName() {
asList(row.get(0, Integer.class), row.get(1,Integer.class)),
connection.createStatement(
"SELECT x, y FROM testNullBindByName WHERE x = 3 ORDER BY y"));

// When the same name is used for multiple parameters, expect a value
// bound to that name to be set as the value for all of those parameters.
// Expect a value bound to the index of one of those parameters to be
// set only for the parameter at that index.
awaitOne(connection.createStatement(
"DELETE FROM testNullBindByName WHERE x IS NULL AND y IS NULL")
.execute());
awaitUpdate(asList(1, 1, 1),
connection
.createStatement(
"INSERT INTO testNullBindByName VALUES (:same, :same)")
.bindNull("same", Integer.class).add()
.bindNull("same", Integer.class).bind(0, 4).add()
.bind(0, 5).bindNull(1, Integer.class));
awaitQuery(asList(asList(null, null)),
row ->
asList(row.get(0, Integer.class), row.get(1,Integer.class)),
connection.createStatement(
"SELECT x, y FROM testNullBindByName" +
" WHERE x IS NULL AND y IS NULL"));
awaitQuery(asList(asList(4, null), asList(5, null)),
row ->
asList(row.get(0, Integer.class), row.get(1,Integer.class)),
connection.createStatement(
"SELECT x, y FROM testNullBindByName" +
" WHERE x >= 4 AND x IS NOT NULL AND y IS NULL" +
" ORDER BY x, y"));
}
finally {
awaitExecution(connection.createStatement(
Expand Down

0 comments on commit 409b6ff

Please sign in to comment.