Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 135 additions & 1 deletion subprojects/groovy-sql/src/main/java/groovy/sql/Sql.java
Original file line number Diff line number Diff line change
Expand Up @@ -3226,7 +3226,8 @@ public int call(String sql, List<?> params) throws SQLException {
Connection connection = createConnection();
CallableStatement statement = null;
try {
statement = getCallableStatement(connection, sql, params);
SqlWithParams updated = checkForNamedParams(sql, params);
statement = getCallableStatement(connection, updated.getSql(), updated.getParams());
int i = statement.executeUpdate();
cleanup(statement);
return i;
Expand All @@ -3253,6 +3254,35 @@ public int call(String sql, Object[] params) throws SQLException {
return call(sql, Arrays.asList(params));
}

/**
* A variant of {@link #call(String, List)} useful when providing
* the named parameters as a map. The SQL may contain {@code :name} style
* placeholders looked up against the map.
*
* @param sql the SQL statement
* @param params a map of named parameters
* @return the number of rows updated or 0 for SQL statements that return nothing
* @throws SQLException if a database access error occurs
* @since 6.0.0
*/
public int call(String sql, Map params) throws SQLException {
return call(sql, singletonList(params));
}

/**
* A variant of {@link #call(String, List)} useful when providing the
* named parameters as named arguments (Groovy named-argument syntax).
*
* @param params a map of named parameters
* @param sql the SQL statement
* @return the number of rows updated or 0 for SQL statements that return nothing
* @throws SQLException if a database access error occurs
* @since 6.0.0
*/
public int call(Map params, String sql) throws SQLException {
return call(sql, singletonList(params));
}

/**
* Performs a stored procedure call with the given parameters. The closure
* is called once with all the out parameters.
Expand Down Expand Up @@ -3376,6 +3406,38 @@ public void call(GString gstring, @ClosureParams(value=SimpleType.class, options
call(sql, params, closure);
}

/**
* A variant of {@link #call(String, List, Closure)} useful when providing
* the named parameters as a map. The SQL may contain {@code :name} style
* placeholders looked up against the map. Out parameter markers (e.g.
* {@link #VARCHAR}) can appear as map values at their named positions.
*
* @param sql the SQL statement
* @param params a map of named parameters
* @param closure called once with all out parameter results
* @throws SQLException if a database access error occurs
* @since 6.0.0
*/
public void call(String sql, Map params,
@ClosureParams(value=SimpleType.class, options="java.lang.Object[]") Closure closure) throws SQLException {
call(sql, singletonList(params), closure);
}

/**
* A variant of {@link #call(String, List, Closure)} useful when providing
* the named parameters as named arguments (Groovy named-argument syntax).
*
* @param params a map of named parameters
* @param sql the SQL statement
* @param closure called once with all out parameter results
* @throws SQLException if a database access error occurs
* @since 6.0.0
*/
public void call(Map params, String sql,
@ClosureParams(value=SimpleType.class, options="java.lang.Object[]") Closure closure) throws SQLException {
call(sql, singletonList(params), closure);
}

/**
* Performs a stored procedure call with the given parameters,
* calling the closure once with all result objects,
Expand Down Expand Up @@ -3435,6 +3497,39 @@ public List<GroovyRowResult> callWithRows(String sql, List<?> params, @ClosurePa
return callWithRows(sql, params, FIRST_RESULT_SET, closure).get(0);
}

/**
* A variant of {@link #callWithRows(String, List, Closure)} useful when
* providing the named parameters as a map. The SQL may contain
* {@code :name} style placeholders looked up against the map.
*
* @param sql the SQL statement
* @param params a map of named parameters
* @param closure called once with all out parameter results
* @return a list of GroovyRowResult objects
* @throws SQLException if a database access error occurs
* @since 6.0.0
*/
public List<GroovyRowResult> callWithRows(String sql, Map params,
@ClosureParams(value=SimpleType.class, options="java.lang.Object[]") Closure closure) throws SQLException {
return callWithRows(sql, singletonList(params), closure);
}

/**
* A variant of {@link #callWithRows(String, List, Closure)} useful when
* providing the named parameters as named arguments (Groovy named-argument syntax).
*
* @param params a map of named parameters
* @param sql the SQL statement
* @param closure called once with all out parameter results
* @return a list of GroovyRowResult objects
* @throws SQLException if a database access error occurs
* @since 6.0.0
*/
public List<GroovyRowResult> callWithRows(Map params, String sql,
@ClosureParams(value=SimpleType.class, options="java.lang.Object[]") Closure closure) throws SQLException {
return callWithRows(sql, singletonList(params), closure);
}

/**
* Performs a stored procedure call with the given parameters,
* calling the closure once with all result objects,
Expand Down Expand Up @@ -3494,6 +3589,39 @@ public List<List<GroovyRowResult>> callWithAllRows(String sql, List<?> params, @
return callWithRows(sql, params, ALL_RESULT_SETS, closure);
}

/**
* A variant of {@link #callWithAllRows(String, List, Closure)} useful when
* providing the named parameters as a map. The SQL may contain
* {@code :name} style placeholders looked up against the map.
*
* @param sql the SQL statement
* @param params a map of named parameters
* @param closure called once with all out parameter results
* @return a list containing lists of GroovyRowResult objects
* @throws SQLException if a database access error occurs
* @since 6.0.0
*/
public List<List<GroovyRowResult>> callWithAllRows(String sql, Map params,
@ClosureParams(value=SimpleType.class, options="java.lang.Object[]") Closure closure) throws SQLException {
return callWithAllRows(sql, singletonList(params), closure);
}

/**
* A variant of {@link #callWithAllRows(String, List, Closure)} useful when
* providing the named parameters as named arguments (Groovy named-argument syntax).
*
* @param params a map of named parameters
* @param sql the SQL statement
* @param closure called once with all out parameter results
* @return a list containing lists of GroovyRowResult objects
* @throws SQLException if a database access error occurs
* @since 6.0.0
*/
public List<List<GroovyRowResult>> callWithAllRows(Map params, String sql,
@ClosureParams(value=SimpleType.class, options="java.lang.Object[]") Closure closure) throws SQLException {
return callWithAllRows(sql, singletonList(params), closure);
}

/**
* If this SQL object was created with a Connection then this method closes
* the connection. If this SQL object was created from a DataSource then
Expand Down Expand Up @@ -4025,6 +4153,12 @@ protected List<List<GroovyRowResult>> callWithRows(String sql, List<?> params, i
CallableStatement statement = null;
List<GroovyResultSet> resultSetResources = new ArrayList<>();
try {
// Resolve :name-style placeholders before binding and before the
// OutParameter scan below — the scan needs to see markers in their
// final positional order, not inside a still-wrapped Map.
SqlWithParams updated = checkForNamedParams(sql, params);
sql = updated.getSql();
params = updated.getParams();
statement = getCallableStatement(connection, sql, params);
boolean hasResultSet = statement.execute();
List<Object> results = new ArrayList<>();
Expand Down
20 changes: 20 additions & 0 deletions subprojects/groovy-sql/src/spec/doc/sql-userguide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,26 @@ as parameters to the method call. For output parameters, the resulting type must
include::../test/SqlTest.groovy[tags=sql_use_stored_proc_inout,indent=0]
----

The `call`, `callWithRows`, and `callWithAllRows` methods also accept a `Map` of named parameters. Use
`{call NAME(:paramName)}` placeholders in the SQL and supply the values — including any output-parameter
type markers — as map entries. Map iteration order doesn't matter; bindings are resolved by name against
the `:name` tokens in the SQL:

[source,groovy]
.Using a stored procedure with a map of named parameters
----
include::../test/SqlTest.groovy[tags=sql_use_stored_proc_inout_map,indent=0]
----

The same overloads support Groovy's named-argument call syntax (map entries written as
`name: value` pairs before the SQL string):

[source,groovy]
.Using a stored procedure with Groovy named-argument syntax
----
include::../test/SqlTest.groovy[tags=sql_use_stored_proc_inout_named_args,indent=0]
----

[source,groovy]
.Creating a stored procedure with an input/output parameter
----
Expand Down
15 changes: 15 additions & 0 deletions subprojects/groovy-sql/src/spec/test/SqlTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,21 @@ class SqlTest extends GroovyTestCase {
fullname -> assert fullname == 'Dierk Koenig'
}
// end::sql_use_stored_proc_inout[]

// tag::sql_use_stored_proc_inout_map[]
sql.call('{call CONCAT_NAME(:fullname, :first, :last)}',
[fullname: Sql.VARCHAR, first: 'Dierk', last: 'Koenig']) {
fullname -> assert fullname == 'Dierk Koenig'
}
// end::sql_use_stored_proc_inout_map[]

// tag::sql_use_stored_proc_inout_named_args[]
sql.call(fullname: Sql.VARCHAR, first: 'Dierk', last: 'Koenig',
'{call CONCAT_NAME(:fullname, :first, :last)}') {
fullname -> assert fullname == 'Dierk Koenig'
}
// end::sql_use_stored_proc_inout_named_args[]

sql.execute "DROP PROCEDURE CONCAT_NAME"
}
'''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,102 @@ class SqlCallTest extends GroovyTestCase {
assert lastRows[0].lastname == 'Ventura'
}

// GROOVY-11936
void testCallUsingMapNamedParams() {
String found
sql.call('{call FindByFirst(:first, :answer)}',
[first: 'James', answer: Sql.VARCHAR]) { ans ->
found = ans
}
assert found == 'Last Name is Strachan'
}

// GROOVY-11936
void testCallUsingMapFirstNamedArgs() {
String found
sql.call(first: 'Bob', answer: Sql.VARCHAR,
'{call FindByFirst(:first, :answer)}') { ans ->
found = ans
}
assert found == 'Last Name is Mcwhirter'
}

// GROOVY-11936
void testCallWithRowsUsingMap() {
String found
def rows = sql.callWithRows('{call FindAllByFirstWithTotal(:first, :total)}',
[first: 'J', total: Sql.VARCHAR]) { total ->
found = total
}
assert found == 'Found total 2'
assert rows.size() == 2
assert rows[0].firstname == 'James'
assert rows[1].firstname == 'Jean'
}

// GROOVY-11936
void testCallWithRowsUsingMapFirstNamedArgs() {
String found
def rows = sql.callWithRows(first: 'J', total: Sql.VARCHAR,
'{call FindAllByFirstWithTotal(:first, :total)}') { total ->
found = total
}
assert found == 'Found total 2'
assert rows.size() == 2
}

// GROOVY-11936
void testCallWithAllRowsUsingMap() {
String foundFirst
String foundLast
def rowList = sql.callWithAllRows(
'{call FindAllByFirstAndFindAllByLastWithTotals(:first, :last, :firstOut, :lastOut)}',
[first: 'J', last: 'V', firstOut: Sql.VARCHAR, lastOut: Sql.VARCHAR]) { firstTotal, lastTotal ->
foundFirst = firstTotal
foundLast = lastTotal
}
assert foundFirst == 'Found total 2'
assert foundLast == 'Found total 1'
assert rowList.size() == 2
assert rowList[0]*.firstname == ['James', 'Jean']
assert rowList[1]*.firstname == ['Lino']
}

// GROOVY-11936
void testCallWithAllRowsUsingMapFirstNamedArgs() {
String foundFirst
String foundLast
def rowList = sql.callWithAllRows(
first: 'J', last: 'V', firstOut: Sql.VARCHAR, lastOut: Sql.VARCHAR,
'{call FindAllByFirstAndFindAllByLastWithTotals(:first, :last, :firstOut, :lastOut)}') { firstTotal, lastTotal ->
foundFirst = firstTotal
foundLast = lastTotal
}
assert foundFirst == 'Found total 2'
assert foundLast == 'Found total 1'
assert rowList.size() == 2
}

// GROOVY-11936: Map values for IN-only stored procedure calls
void testCallWithAllRowsUsingMapInOnly() {
// FindAllByFirst has no OUT params — just IN and a ResultSet.
def rowList = sql.callWithAllRows('{call FindAllByFirst(:first)}',
[first: 'J']) { /* no out params */ }
assert rowList.size() == 1
assert rowList[0]*.firstname == ['James', 'Jean']
}

// GROOVY-11936: Map-key order should not affect binding — SQL text order wins.
void testCallMapKeyOrderIndependent() {
String found
// Reverse key order vs SQL occurrence order:
sql.call('{call FindByFirst(:first, :answer)}',
[answer: Sql.VARCHAR, first: 'Sam']) { ans ->
found = ans
}
assert found == 'Last Name is Pullara'
}

// GROOVY-7768
void testCallWithAllRowsWithInputParamOnly() {
List<List<GroovyRowResult>> rowList = sql.callWithAllRows('{call FindAllByFirst(?)}', ['J'], { /* no out params */ })
Expand Down
Loading