diff --git a/docs/docs/en/guide/task/sql.md b/docs/docs/en/guide/task/sql.md index cb662da28cfe..e2ee9a073d0f 100644 --- a/docs/docs/en/guide/task/sql.md +++ b/docs/docs/en/guide/task/sql.md @@ -27,6 +27,15 @@ Refer to [datasource-setting](../installation/datasource-setting.md) `DataSource | Pre-SQL | Pre-SQL executes before the SQL statement. | | Post-SQL | Post-SQL executes after the SQL statement. | +## Parameter Rendering + +SQL task replaces custom-parameter placeholders in the SQL text before submitting it to the datasource through JDBC `Statement`. + +- Both `${name}` and `!{name}` insert the parameter value as text. DolphinScheduler does not add quotes, escape characters, interpret the parameter type, or otherwise adapt the value to a database dialect. +- Write datasource-specific SQL syntax, including any required quotes, in the SQL statement or parameter value. Only use trusted parameter values because text replacement does not prevent SQL injection. +- Rendering scans the source SQL once. Placeholder-like text introduced by a parameter value is not rendered again. +- If a source placeholder is unclosed or refers to a missing custom parameter, the SQL task fails before submitting the statement to the datasource. + ## Task Example ### Hive Table Create Example diff --git a/docs/docs/zh/guide/task/sql.md b/docs/docs/zh/guide/task/sql.md index 063e332147b4..6eace638be87 100644 --- a/docs/docs/zh/guide/task/sql.md +++ b/docs/docs/zh/guide/task/sql.md @@ -27,6 +27,15 @@ SQL任务类型,用于连接数据库并执行相应SQL。 - 前置sql:前置sql在sql语句之前执行。 - 后置sql:后置sql在sql语句之后执行。 +## 参数渲染 + +SQL 任务会先在 SQL 文本中替换自定义参数占位符,然后通过 JDBC `Statement` 提交到数据源执行。 + +- `${name}` 和 `!{name}` 都会将参数值作为文本插入。DolphinScheduler 不会自动加引号、转义字符、解释参数类型,也不会按数据库方言转换参数值。 +- 请在 SQL 语句或参数值中编写数据源所需的 SQL 语法,包括必要的引号。文本替换无法防止 SQL 注入,因此只能使用可信的参数值。 +- 渲染只扫描一次源 SQL。参数值中包含的占位符形式文本不会再次渲染。 +- 如果源 SQL 中的占位符未闭合或引用了不存在的自定义参数,SQL 任务会在向数据源提交语句前失败。 + ## 任务样例 ### Hive表创建示例 diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlBinds.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlBinds.java index 3b61a53626f1..39ca25aff523 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlBinds.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlBinds.java @@ -22,7 +22,7 @@ import java.util.Map; /** - * Used to contains both prepared sql string and its to-be-bind parameters + * Used to contain the rendered SQL string and legacy parameter map. */ public class SqlBinds { diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTask.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTask.java index 113dfbcd9eb8..0488b70b6922 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTask.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTask.java @@ -47,7 +47,6 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.sql.Connection; -import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; @@ -60,8 +59,6 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -335,9 +332,9 @@ private void sendAttachment(int groupId, String title, String content) { } private String executeQuery(Connection connection, SqlBinds sqlBinds, String handlerType) throws Exception { - try (PreparedStatement statement = prepareStatementAndBind(connection, sqlBinds)) { + try (Statement statement = createStatement(connection)) { log.info("{} statement execute query, for sql: {}", handlerType, sqlBinds.getSql()); - ResultSet resultSet = statement.executeQuery(); + ResultSet resultSet = statement.executeQuery(sqlBinds.getSql()); return resultProcess(resultSet); } } @@ -346,8 +343,8 @@ private String executeUpdate(Connection connection, List statementsBin String handlerType) throws Exception { int result = 0; for (SqlBinds sqlBind : statementsBinds) { - try (PreparedStatement tmpStatement = prepareStatementAndBind(connection, sqlBind)) { - result = tmpStatement.executeUpdate(); + try (Statement tmpStatement = createStatement(connection)) { + result = tmpStatement.executeUpdate(sqlBind.getSql()); log.info("{} statement execute update result: {}, for sql: {}", handlerType, result, sqlBind.getSql()); } @@ -356,61 +353,27 @@ private String executeUpdate(Connection connection, List statementsBin } /** - * preparedStatement bind + * create statement * * @param connection connection - * @param sqlBinds sqlBinds - * @return PreparedStatement - * @throws Exception Exception + * @return Statement */ - private PreparedStatement prepareStatementAndBind(Connection connection, SqlBinds sqlBinds) { + private Statement createStatement(Connection connection) { // is the timeout set // todo: we need control the timeout at master side. boolean timeoutFlag = taskExecutionContext.getTaskTimeoutStrategy() == TaskTimeoutStrategy.FAILED || taskExecutionContext.getTaskTimeoutStrategy() == TaskTimeoutStrategy.WARNFAILED; try { - PreparedStatement stmt = connection.prepareStatement(sqlBinds.getSql()); + Statement stmt = connection.createStatement(); if (timeoutFlag) { stmt.setQueryTimeout(taskExecutionContext.getTaskTimeout()); } stmt.setMaxRows(sqlParameters.getLimit() <= 0 ? QUERY_LIMIT : sqlParameters.getLimit()); - Map params = sqlBinds.getParamsMap(); - if (params != null) { - for (Map.Entry entry : params.entrySet()) { - Property prop = entry.getValue(); - ParameterUtils.setInParameter(entry.getKey(), stmt, prop.getType(), prop.getValue()); - } - } - log.info("prepare statement replace sql : {}, sql parameters : {}", sqlBinds.getSql(), - sqlBinds.getParamsMap()); sessionStatement = stmt; return stmt; } catch (Exception exception) { - throw new TaskException("SQL task prepareStatementAndBind error", exception); - } - } - - /** - * print replace sql - * - * @param content content - * @param formatSql format sql - * @param rgex rgex - * @param sqlParamsMap sql params map - */ - private void printReplacedSql(String content, String formatSql, String rgex, Map sqlParamsMap) { - // parameter print style - log.info("after replace sql , preparing : {}", formatSql); - StringBuilder logPrint = new StringBuilder("replaced sql , parameters:"); - if (sqlParamsMap == null) { - log.info("printReplacedSql: sqlParamsMap is null."); - } else { - for (int i = 1; i <= sqlParamsMap.size(); i++) { - logPrint.append(sqlParamsMap.get(i).getValue()).append("(").append(sqlParamsMap.get(i).getType()) - .append(")"); - } + throw new TaskException("SQL task createStatement error", exception); } - log.info("Sql Params are {}", logPrint); } private void ensureSqlContent() { @@ -445,9 +408,6 @@ private void ensureSqlContent() { * @return SqlBinds */ private SqlBinds getSqlAndSqlParamsMap(String sql) { - Map sqlParamsMap = new HashMap<>(); - StringBuilder sqlBuilder = new StringBuilder(); - // new // replace variable TIME with $[YYYYmmddd...] in sql when history run job and batch complement job sql = ParameterUtils.replaceScheduleTime(sql, DateUtils.timeStampToDate(taskExecutionContext.getScheduleTime())); @@ -464,38 +424,9 @@ private SqlBinds getSqlAndSqlParamsMap(String sql) { sqlParameters.setTitle(title); } - // spell SQL according to the final user-defined variable - if (paramsMap == null) { - sqlBuilder.append(sql); - return new SqlBinds(sqlBuilder.toString(), sqlParamsMap); - } - - // special characters need to be escaped, ${} needs to be escaped - setSqlParamsMap(sql, sqlParamsMap, paramsMap, taskExecutionContext.getTaskInstanceId()); - // Replace the original value in sql !{...} ,Does not participate in precompilation - String rgexo = "['\"]*\\!\\{(.*?)\\}['\"]*"; - sql = replaceOriginalValue(sql, rgexo, paramsMap); - // replace the ${} of the SQL statement with the Placeholder - // Convert the list parameter - String formatSql = ParameterUtils.expandListParameter(sqlParamsMap, sql); - sqlBuilder.append(formatSql); - // print replace sql - printReplacedSql(sql, formatSql, TaskConstants.SQL_PARAMS_REGEX, sqlParamsMap); - return new SqlBinds(sqlBuilder.toString(), sqlParamsMap); - } - - private String replaceOriginalValue(String content, String rgex, Map sqlParamsMap) { - Pattern pattern = Pattern.compile(rgex); - while (true) { - Matcher m = pattern.matcher(content); - if (!m.find()) { - break; - } - String paramName = m.group(1); - String paramValue = sqlParamsMap.get(paramName).getValue(); - content = m.replaceFirst(paramValue); - } - return content; + String renderedSql = SqlTaskParameterRenderer.render(sql, paramsMap, taskExecutionContext.getTaskInstanceId()); + log.info("rendered sql : {}", renderedSql); + return new SqlBinds(renderedSql, Collections.emptyMap()); } } diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTaskParameterRenderer.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTaskParameterRenderer.java new file mode 100644 index 000000000000..75cb8f36e1cd --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/main/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTaskParameterRenderer.java @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.sql; + +import org.apache.dolphinscheduler.plugin.task.api.TaskException; +import org.apache.dolphinscheduler.plugin.task.api.model.Property; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Map; + +final class SqlTaskParameterRenderer { + + private SqlTaskParameterRenderer() { + } + + static String render(String sql, Map paramsMap, int taskInstanceId) { + if (StringUtils.isEmpty(sql)) { + return sql; + } + return renderParameters(sql, paramsMap, taskInstanceId); + } + + private static String renderParameters(String sql, Map paramsMap, int taskInstanceId) { + StringBuilder renderedSql = new StringBuilder(sql.length()); + int index = 0; + while (index < sql.length()) { + char marker = sql.charAt(index); + if (!isPlaceholderStart(sql, index, marker)) { + renderedSql.append(marker); + index++; + continue; + } + + int end = sql.indexOf('}', index + 2); + if (end < 0) { + throw new TaskException(String.format( + "Unclosed SQL parameter placeholder in task instance with id: %s", + taskInstanceId)); + } + + String paramName = sql.substring(index + 2, end); + Property property = getProperty(paramsMap, paramName, taskInstanceId); + renderedSql.append(StringUtils.defaultString(property.getValue())); + index = end + 1; + } + return renderedSql.toString(); + } + + private static boolean isPlaceholderStart(String sql, int index, char marker) { + return (marker == '$' || marker == '!') + && index + 1 < sql.length() + && sql.charAt(index + 1) == '{'; + } + + private static Property getProperty(Map paramsMap, String paramName, int taskInstanceId) { + Property property = paramsMap == null ? null : paramsMap.get(paramName); + if (property == null) { + throw new TaskException(String.format( + "No Property with paramName: %s is found in paramsMap of task instance with id: %s", + paramName, + taskInstanceId)); + } + return property; + } +} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/test/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTaskTest.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/test/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTaskTest.java index 92f0fa0702d2..b7878bda6ae7 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/test/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTaskTest.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-sql/src/test/java/org/apache/dolphinscheduler/plugin/task/sql/SqlTaskTest.java @@ -17,7 +17,10 @@ package org.apache.dolphinscheduler.plugin.task.sql; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.apache.dolphinscheduler.common.utils.DateUtils; @@ -41,8 +44,11 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.sql.Connection; import java.sql.ResultSet; import java.sql.ResultSetMetaData; +import java.sql.Statement; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -420,12 +426,12 @@ void testGetSqlAndSqlParamsMap_withPrepareParamsMap_coversPrintReplacedSql() thr Method method = SqlTask.class.getDeclaredMethod("getSqlAndSqlParamsMap", String.class); method.setAccessible(true); - String inputSql = "select * from student where dt=${dt}"; + String inputSql = "select * from student where dt='${dt}'"; SqlBinds binds = (SqlBinds) method.invoke(task, inputSql); - Assertions.assertEquals("select * from student where dt=?", binds.getSql()); + Assertions.assertEquals("select * from student where dt='1970'", binds.getSql()); Assertions.assertNotNull(binds.getParamsMap()); - Assertions.assertEquals("1970", binds.getParamsMap().get(1).getValue()); + Assertions.assertTrue(binds.getParamsMap().isEmpty()); } @Test @@ -479,6 +485,243 @@ void testEnsureSqlContent_whenResourceMissing_throwsTaskException(@TempDir Path Assertions.assertInstanceOf(TaskException.class, thrown.getCause()); } + @Test + void testSqlTaskLocalRenderer_replacesParametersAsText() throws Exception { + Map prepareParamsMap = new HashMap<>(); + prepareParamsMap.put("dd", new Property("dd", Direct.IN, DataType.VARCHAR, "20250411")); + prepareParamsMap.put("name", new Property("name", Direct.IN, DataType.VARCHAR, "E'O\\Reilly'")); + prepareParamsMap.put("ids", new Property("ids", Direct.IN, DataType.LIST, "1,'x',3")); + prepareParamsMap.put("enabled", new Property("enabled", Direct.IN, DataType.BOOLEAN, "TRUE")); + + TaskExecutionContext ctx = new TaskExecutionContext(); + ctx.setTaskParams("{\"type\":\"HIVE\",\"datasource\":1,\"sql\":\"select 1\"}"); + ctx.setScheduleTime(System.currentTimeMillis()); + ctx.setTaskInstanceId(1); + ctx.setResourceParametersHelper(getResourceParametersHelperWithDatasourceType(DbType.HIVE)); + ctx.setPrepareParamsMap(prepareParamsMap); + + SqlTask task = new SqlTask(ctx); + + Method method = SqlTask.class.getDeclaredMethod("getSqlAndSqlParamsMap", String.class); + method.setAccessible(true); + + String inputSql = "create table test_${dd} as select * from user where name=${name} " + + "and id in (${ids}) and enabled=${enabled}"; + SqlBinds binds = (SqlBinds) method.invoke(task, inputSql); + + Assertions.assertEquals( + "create table test_20250411 as select * from user where name=E'O\\Reilly' " + + "and id in (1,'x',3) and enabled=TRUE", + binds.getSql()); + Assertions.assertTrue(binds.getParamsMap().isEmpty()); + } + + @Test + void testSqlTaskLocalRenderer_preservesSqlAuthoredQuotesAroundParameters() throws Exception { + Map prepareParamsMap = new HashMap<>(); + prepareParamsMap.put("dt", new Property("dt", Direct.IN, DataType.DATE, "2026-07-06")); + prepareParamsMap.put("name", new Property("name", Direct.IN, DataType.VARCHAR, "O'Reilly")); + + TaskExecutionContext ctx = new TaskExecutionContext(); + ctx.setTaskParams("{\"type\":\"HIVE\",\"datasource\":1,\"sql\":\"select 1\"}"); + ctx.setScheduleTime(System.currentTimeMillis()); + ctx.setTaskInstanceId(1); + ctx.setResourceParametersHelper(getResourceParametersHelperWithDatasourceType(DbType.HIVE)); + ctx.setPrepareParamsMap(prepareParamsMap); + + SqlTask task = new SqlTask(ctx); + + Method method = SqlTask.class.getDeclaredMethod("getSqlAndSqlParamsMap", String.class); + method.setAccessible(true); + + SqlBinds binds = (SqlBinds) method.invoke(task, + "select * from student where dt='${dt}' and name='${name}'"); + + Assertions.assertEquals("select * from student where dt='2026-07-06' and name='O'Reilly'", + binds.getSql()); + } + + @Test + void testSqlTaskLocalRenderer_replacesRawPlaceholderWithDollarAndBackslashCharacters() throws Exception { + Map prepareParamsMap = new HashMap<>(); + prepareParamsMap.put("partition", new Property("partition", Direct.IN, DataType.VARCHAR, + "dt='$[yyyyMMdd]' and path='s3://bucket/a\\b'")); + + TaskExecutionContext ctx = new TaskExecutionContext(); + ctx.setTaskParams("{\"type\":\"HIVE\",\"datasource\":1,\"sql\":\"select 1\"}"); + ctx.setScheduleTime(System.currentTimeMillis()); + ctx.setTaskInstanceId(1); + ctx.setResourceParametersHelper(getResourceParametersHelperWithDatasourceType(DbType.HIVE)); + ctx.setPrepareParamsMap(prepareParamsMap); + + SqlTask task = new SqlTask(ctx); + + Method method = SqlTask.class.getDeclaredMethod("getSqlAndSqlParamsMap", String.class); + method.setAccessible(true); + + SqlBinds binds = (SqlBinds) method.invoke(task, + "alter table t add if not exists partition (!{partition})"); + + Assertions.assertEquals( + "alter table t add if not exists partition (dt='$[yyyyMMdd]' and path='s3://bucket/a\\b')", + binds.getSql()); + } + + @Test + void testExecuteQueryUsesStatementWithRenderedSql() throws Exception { + Connection connection = mock(Connection.class); + Statement statement = mock(Statement.class); + ResultSet resultSet = mock(ResultSet.class); + ResultSetMetaData metaData = mock(ResultSetMetaData.class); + + when(connection.createStatement()).thenReturn(statement); + when(statement.executeQuery("select 1 as id")).thenReturn(resultSet); + when(resultSet.getMetaData()).thenReturn(metaData); + when(metaData.getColumnCount()).thenReturn(1); + when(metaData.getColumnLabel(1)).thenReturn("id"); + when(resultSet.next()).thenReturn(false); + + Method method = SqlTask.class.getDeclaredMethod("executeQuery", Connection.class, SqlBinds.class, String.class); + method.setAccessible(true); + + String result = (String) method.invoke(sqlTask, connection, + new SqlBinds("select 1 as id", new HashMap<>()), "main"); + + Assertions.assertEquals("[{\"id\":\"\"}]", result); + verify(connection).createStatement(); + verify(connection, never()).prepareStatement(anyString()); + verify(statement).executeQuery("select 1 as id"); + } + + @Test + void testSqlTaskLocalRenderer_doesNotRescanRawReplacementForSqlParameters() throws Exception { + Map prepareParamsMap = new HashMap<>(); + prepareParamsMap.put("fragment", new Property("fragment", Direct.IN, DataType.VARCHAR, + "dt='${hiveconf:dt}'")); + + TaskExecutionContext ctx = new TaskExecutionContext(); + ctx.setTaskParams("{\"type\":\"HIVE\",\"datasource\":1,\"sql\":\"select 1\"}"); + ctx.setScheduleTime(System.currentTimeMillis()); + ctx.setTaskInstanceId(1); + ctx.setResourceParametersHelper(getResourceParametersHelperWithDatasourceType(DbType.HIVE)); + ctx.setPrepareParamsMap(prepareParamsMap); + + SqlTask task = new SqlTask(ctx); + + Method method = SqlTask.class.getDeclaredMethod("getSqlAndSqlParamsMap", String.class); + method.setAccessible(true); + + SqlBinds binds = (SqlBinds) method.invoke(task, + "alter table t add if not exists partition (!{fragment})"); + + Assertions.assertEquals( + "alter table t add if not exists partition (dt='${hiveconf:dt}')", + binds.getSql()); + } + + @Test + void testSqlTaskLocalRenderer_doesNotRescanTextParameterValueForRawPlaceholder() throws Exception { + Map prepareParamsMap = new HashMap<>(); + prepareParamsMap.put("name", new Property("name", Direct.IN, DataType.VARCHAR, "!{fragment}")); + prepareParamsMap.put("fragment", new Property("fragment", Direct.IN, DataType.VARCHAR, "unsafe_sql")); + + TaskExecutionContext ctx = new TaskExecutionContext(); + ctx.setTaskParams("{\"type\":\"HIVE\",\"datasource\":1,\"sql\":\"select 1\"}"); + ctx.setScheduleTime(System.currentTimeMillis()); + ctx.setTaskInstanceId(1); + ctx.setResourceParametersHelper(getResourceParametersHelperWithDatasourceType(DbType.HIVE)); + ctx.setPrepareParamsMap(prepareParamsMap); + + SqlTask task = new SqlTask(ctx); + + Method method = SqlTask.class.getDeclaredMethod("getSqlAndSqlParamsMap", String.class); + method.setAccessible(true); + + SqlBinds binds = (SqlBinds) method.invoke(task, + "select * from student where name=${name}"); + + Assertions.assertEquals("select * from student where name=!{fragment}", binds.getSql()); + } + + @Test + void testSqlTaskLocalRenderer_replacesPlaceholderInsideJsonStringLiteral() throws Exception { + Map prepareParamsMap = new HashMap<>(); + prepareParamsMap.put("name", new Property("name", Direct.IN, DataType.VARCHAR, "O'Reilly")); + + TaskExecutionContext ctx = new TaskExecutionContext(); + ctx.setTaskParams("{\"type\":\"HIVE\",\"datasource\":1,\"sql\":\"select 1\"}"); + ctx.setScheduleTime(System.currentTimeMillis()); + ctx.setTaskInstanceId(1); + ctx.setResourceParametersHelper(getResourceParametersHelperWithDatasourceType(DbType.HIVE)); + ctx.setPrepareParamsMap(prepareParamsMap); + + SqlTask task = new SqlTask(ctx); + + Method method = SqlTask.class.getDeclaredMethod("getSqlAndSqlParamsMap", String.class); + method.setAccessible(true); + + SqlBinds binds = (SqlBinds) method.invoke(task, + "select '{\"name\":\"${name}\"}' as payload"); + + Assertions.assertEquals("select '{\"name\":\"O'Reilly\"}' as payload", binds.getSql()); + } + + @Test + void testSqlTaskLocalRenderer_allowsDatasourceSpecificIdentifierFragment() { + Map prepareParamsMap = new HashMap<>(); + prepareParamsMap.put("suffix", new Property("suffix", Direct.IN, DataType.VARCHAR, "2025-04-11")); + + Assertions.assertEquals( + "create table test_2025-04-11", + SqlTaskParameterRenderer.render("create table test_${suffix}", prepareParamsMap, 1)); + } + + @Test + void testSqlTaskLocalRenderer_doesNotInterpretParameterType() { + Map prepareParamsMap = new HashMap<>(); + prepareParamsMap.put("id", new Property("id", Direct.IN, DataType.INTEGER, "1 OR 1=1")); + + Assertions.assertEquals( + "select * from student where id=1 OR 1=1", + SqlTaskParameterRenderer.render("select * from student where id=${id}", prepareParamsMap, 1)); + } + + @Test + void testSqlTaskLocalRenderer_rejectsMissingParametersWhenMapIsEmpty() { + Assertions.assertAll( + () -> Assertions.assertThrows(TaskException.class, + () -> SqlTaskParameterRenderer.render( + "select * from student where id=${missing}", Collections.emptyMap(), 1)), + () -> Assertions.assertThrows(TaskException.class, + () -> SqlTaskParameterRenderer.render( + "select * from student where id=!{missing}", Collections.emptyMap(), 1))); + } + + @Test + void testSqlTaskLocalRenderer_rejectsMissingParameterWhenMapIsNotEmpty() { + Map prepareParamsMap = new HashMap<>(); + prepareParamsMap.put("other", new Property("other", Direct.IN, DataType.VARCHAR, "value")); + + Assertions.assertThrows( + TaskException.class, + () -> SqlTaskParameterRenderer.render( + "select * from student where id=${missing}", prepareParamsMap, 1)); + } + + @Test + void testSqlTaskLocalRenderer_rejectsUnclosedSourcePlaceholder() { + Map prepareParamsMap = new HashMap<>(); + prepareParamsMap.put("id", new Property("id", Direct.IN, DataType.INTEGER, "1")); + + Assertions.assertAll( + () -> Assertions.assertThrows(TaskException.class, + () -> SqlTaskParameterRenderer.render( + "select * from student where id=${id", prepareParamsMap, 1)), + () -> Assertions.assertThrows(TaskException.class, + () -> SqlTaskParameterRenderer.render( + "select * from student where id=!{id", prepareParamsMap, 1))); + } + private ResourceParametersHelper getResourceParametersHelperWithDatasourceType(DbType dbType) { DataSourceParameters parameters = new DataSourceParameters(); parameters.setType(dbType);