diff --git a/src/main/java/org/apache/ibatis/builder/ParameterMappingTokenHandler.java b/src/main/java/org/apache/ibatis/builder/ParameterMappingTokenHandler.java new file mode 100644 index 00000000000..83b76279c8a --- /dev/null +++ b/src/main/java/org/apache/ibatis/builder/ParameterMappingTokenHandler.java @@ -0,0 +1,151 @@ +/* + * Copyright 2009-2025 the original author or authors. + * + * Licensed 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 + * + * https://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.ibatis.builder; + +import java.util.List; +import java.util.Map; + +import org.apache.ibatis.mapping.ParameterMapping; +import org.apache.ibatis.mapping.ParameterMode; +import org.apache.ibatis.parsing.TokenHandler; +import org.apache.ibatis.reflection.MetaClass; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.property.PropertyTokenizer; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.type.JdbcType; + +public class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler { + + private static final String PARAMETER_PROPERTIES = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName"; + private final List parameterMappings; + private final Class parameterType; + private final MetaObject metaParameters; + private final Object parameterObject; + private final boolean paramExists; + + public ParameterMappingTokenHandler(List parameterMappings, Configuration configuration, + Object parameterObject, Class parameterType, Map additionalParameters, boolean paramExists) { + super(configuration); + this.parameterType = parameterObject == null ? (parameterType == null ? Object.class : parameterType) + : parameterObject.getClass(); + this.metaParameters = configuration.newMetaObject(additionalParameters); + this.parameterObject = parameterObject; + this.paramExists = paramExists; + this.parameterMappings = parameterMappings; + } + + public ParameterMappingTokenHandler(List parameterMappings, Configuration configuration, + Class parameterType, Map additionalParameters) { + super(configuration); + this.parameterType = parameterType; + this.metaParameters = configuration.newMetaObject(additionalParameters); + this.parameterObject = null; + this.paramExists = false; + this.parameterMappings = parameterMappings; + } + + public List getParameterMappings() { + return parameterMappings; + } + + @Override + public String handleToken(String content) { + parameterMappings.add(buildParameterMapping(content)); + return "?"; + } + + private ParameterMapping buildParameterMapping(String content) { + Map propertiesMap = parseParameterMapping(content); + String property = propertiesMap.get("property"); + PropertyTokenizer propertyTokenizer = new PropertyTokenizer(property); + Class propertyType; + if (metaParameters.hasGetter(propertyTokenizer.getName())) { // issue #448 get type from additional params + propertyType = metaParameters.getGetterType(property); + } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) { + propertyType = parameterType; + } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) { + propertyType = java.sql.ResultSet.class; + } else if (property == null || Map.class.isAssignableFrom(parameterType)) { + propertyType = Object.class; + } else { + MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory()); + if (metaClass.hasGetter(property)) { + propertyType = metaClass.getGetterType(property); + } else { + propertyType = Object.class; + } + } + ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType); + Class javaType = propertyType; + String typeHandlerAlias = null; + ParameterMode mode = null; + for (Map.Entry entry : propertiesMap.entrySet()) { + String name = entry.getKey(); + String value = entry.getValue(); + if ("javaType".equals(name)) { + javaType = resolveClass(value); + builder.javaType(javaType); + } else if ("jdbcType".equals(name)) { + builder.jdbcType(resolveJdbcType(value)); + } else if ("mode".equals(name)) { + mode = resolveParameterMode(value); + builder.mode(mode); + } else if ("numericScale".equals(name)) { + builder.numericScale(Integer.valueOf(value)); + } else if ("resultMap".equals(name)) { + builder.resultMapId(value); + } else if ("typeHandler".equals(name)) { + typeHandlerAlias = value; + } else if ("jdbcTypeName".equals(name)) { + builder.jdbcTypeName(value); + } else if ("property".equals(name)) { + // Do Nothing + } else if ("expression".equals(name)) { + throw new BuilderException("Expression based parameters are not supported yet"); + } else { + throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + + "}. Valid properties are " + PARAMETER_PROPERTIES); + } + } + if (typeHandlerAlias != null) { + builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias)); + } + if (!ParameterMode.OUT.equals(mode) && paramExists) { + if (metaParameters.hasGetter(propertyTokenizer.getName())) { + builder.value(metaParameters.getValue(property)); + } else if (parameterObject == null) { + builder.value(null); + } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { + builder.value(parameterObject); + } else { + MetaObject metaObject = configuration.newMetaObject(parameterObject); + builder.value(metaObject.getValue(property)); + } + } + return builder.build(); + } + + private Map parseParameterMapping(String content) { + try { + return new ParameterExpression(content); + } catch (BuilderException ex) { + throw ex; + } catch (Exception ex) { + throw new BuilderException("Parsing error was found in mapping #{" + content + + "}. Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex); + } + } +} diff --git a/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java b/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java index d81a1d7cb34..928436b13cd 100644 --- a/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java +++ b/src/main/java/org/apache/ibatis/builder/SqlSourceBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2024 the original author or authors. + * Copyright 2009-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,43 +15,27 @@ */ package org.apache.ibatis.builder; -import java.sql.ResultSet; -import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.StringTokenizer; import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.mapping.SqlSource; -import org.apache.ibatis.parsing.GenericTokenParser; -import org.apache.ibatis.parsing.TokenHandler; -import org.apache.ibatis.reflection.MetaClass; -import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.session.Configuration; -import org.apache.ibatis.type.JdbcType; /** * @author Clinton Begin */ -public class SqlSourceBuilder extends BaseBuilder { +public class SqlSourceBuilder { - private static final String PARAMETER_PROPERTIES = "javaType,jdbcType,mode,numericScale,resultMap,typeHandler,jdbcTypeName"; - - public SqlSourceBuilder(Configuration configuration) { - super(configuration); + private SqlSourceBuilder() { + super(); } - public SqlSource parse(String originalSql, Class parameterType, Map additionalParameters) { - ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, - additionalParameters); - GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); - String sql; - if (configuration.isShrinkWhitespacesInSql()) { - sql = parser.parse(removeExtraWhitespaces(originalSql)); - } else { - sql = parser.parse(originalSql); - } - return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); + public static SqlSource buildSqlSource(Configuration configuration, String sql, + List parameterMappings) { + return new StaticSqlSource(configuration, + configuration.isShrinkWhitespacesInSql() ? SqlSourceBuilder.removeExtraWhitespaces(sql) : sql, + parameterMappings); } public static String removeExtraWhitespaces(String original) { @@ -68,95 +52,4 @@ public static String removeExtraWhitespaces(String original) { return builder.toString(); } - private static class ParameterMappingTokenHandler extends BaseBuilder implements TokenHandler { - - private final List parameterMappings = new ArrayList<>(); - private final Class parameterType; - private final MetaObject metaParameters; - - public ParameterMappingTokenHandler(Configuration configuration, Class parameterType, - Map additionalParameters) { - super(configuration); - this.parameterType = parameterType; - this.metaParameters = configuration.newMetaObject(additionalParameters); - } - - public List getParameterMappings() { - return parameterMappings; - } - - @Override - public String handleToken(String content) { - parameterMappings.add(buildParameterMapping(content)); - return "?"; - } - - private ParameterMapping buildParameterMapping(String content) { - Map propertiesMap = parseParameterMapping(content); - String property = propertiesMap.get("property"); - Class propertyType; - if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params - propertyType = metaParameters.getGetterType(property); - } else if (typeHandlerRegistry.hasTypeHandler(parameterType)) { - propertyType = parameterType; - } else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) { - propertyType = ResultSet.class; - } else if (property == null || Map.class.isAssignableFrom(parameterType)) { - propertyType = Object.class; - } else { - MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory()); - if (metaClass.hasGetter(property)) { - propertyType = metaClass.getGetterType(property); - } else { - propertyType = Object.class; - } - } - ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType); - Class javaType = propertyType; - String typeHandlerAlias = null; - for (Map.Entry entry : propertiesMap.entrySet()) { - String name = entry.getKey(); - String value = entry.getValue(); - if ("javaType".equals(name)) { - javaType = resolveClass(value); - builder.javaType(javaType); - } else if ("jdbcType".equals(name)) { - builder.jdbcType(resolveJdbcType(value)); - } else if ("mode".equals(name)) { - builder.mode(resolveParameterMode(value)); - } else if ("numericScale".equals(name)) { - builder.numericScale(Integer.valueOf(value)); - } else if ("resultMap".equals(name)) { - builder.resultMapId(value); - } else if ("typeHandler".equals(name)) { - typeHandlerAlias = value; - } else if ("jdbcTypeName".equals(name)) { - builder.jdbcTypeName(value); - } else if ("property".equals(name)) { - // Do Nothing - } else if ("expression".equals(name)) { - throw new BuilderException("Expression based parameters are not supported yet"); - } else { - throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content - + "}. Valid properties are " + PARAMETER_PROPERTIES); - } - } - if (typeHandlerAlias != null) { - builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias)); - } - return builder.build(); - } - - private Map parseParameterMapping(String content) { - try { - return new ParameterExpression(content); - } catch (BuilderException ex) { - throw ex; - } catch (Exception ex) { - throw new BuilderException("Parsing error was found in mapping #{" + content - + "}. Check syntax #{property|(expression), var1=value1, var2=value2, ...} ", ex); - } - } - } - } diff --git a/src/main/java/org/apache/ibatis/executor/BaseExecutor.java b/src/main/java/org/apache/ibatis/executor/BaseExecutor.java index 6f79f0958af..509f4e84fcd 100644 --- a/src/main/java/org/apache/ibatis/executor/BaseExecutor.java +++ b/src/main/java/org/apache/ibatis/executor/BaseExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2023 the original author or authors. + * Copyright 2009-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -212,7 +212,9 @@ public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBo if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); - if (boundSql.hasAdditionalParameter(propertyName)) { + if (parameterMapping.hasValue()) { + value = parameterMapping.getValue(); + } else if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; diff --git a/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java b/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java index 16c3d1618bf..7265499c600 100644 --- a/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java +++ b/src/main/java/org/apache/ibatis/mapping/ParameterMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2024 the original author or authors. + * Copyright 2009-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ */ public class ParameterMapping { + private static final Object UNSET = new Object(); private Configuration configuration; private String property; @@ -38,6 +39,7 @@ public class ParameterMapping { private String resultMapId; private String jdbcTypeName; private String expression; + private Object value = UNSET; private ParameterMapping() { } @@ -99,6 +101,11 @@ public Builder expression(String expression) { return this; } + public Builder value(Object value) { + parameterMapping.value = value; + return this; + } + public ParameterMapping build() { resolveTypeHandler(); validate(); @@ -205,6 +212,14 @@ public String getExpression() { return expression; } + public Object getValue() { + return value; + } + + public boolean hasValue() { + return value != UNSET; + } + @Override public String toString() { final StringBuilder sb = new StringBuilder("ParameterMapping{"); @@ -218,6 +233,7 @@ public String toString() { sb.append(", resultMapId='").append(resultMapId).append('\''); sb.append(", jdbcTypeName='").append(jdbcTypeName).append('\''); sb.append(", expression='").append(expression).append('\''); + sb.append(", value='").append(value).append('\''); sb.append('}'); return sb.toString(); } diff --git a/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java b/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java index 63cebcd1486..678c791f92d 100644 --- a/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java +++ b/src/main/java/org/apache/ibatis/scripting/defaults/DefaultParameterHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2023 the original author or authors. + * Copyright 2009-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,6 +45,8 @@ public class DefaultParameterHandler implements ParameterHandler { private final BoundSql boundSql; private final Configuration configuration; + private MetaObject metaObject; + public DefaultParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) { this.mappedStatement = mappedStatement; this.configuration = mappedStatement.getConfiguration(); @@ -63,24 +65,10 @@ public void setParameters(PreparedStatement ps) { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); List parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { - MetaObject metaObject = null; for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { - Object value; - String propertyName = parameterMapping.getProperty(); - if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params - value = boundSql.getAdditionalParameter(propertyName); - } else if (parameterObject == null) { - value = null; - } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { - value = parameterObject; - } else { - if (metaObject == null) { - metaObject = configuration.newMetaObject(parameterObject); - } - value = metaObject.getValue(propertyName); - } + Object value = getValue(parameterMapping); TypeHandler typeHandler = parameterMapping.getTypeHandler(); JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) { @@ -96,4 +84,23 @@ public void setParameters(PreparedStatement ps) { } } + private Object getValue(ParameterMapping parameterMapping) { + if (parameterMapping.hasValue()) { + return parameterMapping.getValue(); + } + String propertyName = parameterMapping.getProperty(); + if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params + return boundSql.getAdditionalParameter(propertyName); + } else if (parameterObject == null) { + return null; + } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { + return parameterObject; + } else { + if (metaObject == null) { + metaObject = configuration.newMetaObject(parameterObject); + } + return metaObject.getValue(propertyName); + } + } + } diff --git a/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java b/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java index f30f6423b2b..ab96078fe50 100644 --- a/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java +++ b/src/main/java/org/apache/ibatis/scripting/defaults/RawSqlSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2023 the original author or authors. + * Copyright 2009-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,16 @@ */ package org.apache.ibatis.scripting.defaults; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; +import org.apache.ibatis.builder.ParameterMappingTokenHandler; import org.apache.ibatis.builder.SqlSourceBuilder; import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.mapping.SqlSource; +import org.apache.ibatis.parsing.GenericTokenParser; import org.apache.ibatis.scripting.xmltags.DynamicContext; import org.apache.ibatis.scripting.xmltags.DynamicSqlSource; import org.apache.ibatis.scripting.xmltags.SqlNode; @@ -37,19 +42,19 @@ public class RawSqlSource implements SqlSource { private final SqlSource sqlSource; public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class parameterType) { - this(configuration, getSql(configuration, rootSqlNode), parameterType); + DynamicContext context = new DynamicContext(configuration, parameterType); + rootSqlNode.apply(context); + String sql = context.getSql(); + sqlSource = SqlSourceBuilder.buildSqlSource(configuration, sql, context.getParameterMappings()); } public RawSqlSource(Configuration configuration, String sql, Class parameterType) { - SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class clazz = parameterType == null ? Object.class : parameterType; - sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>()); - } - - private static String getSql(Configuration configuration, SqlNode rootSqlNode) { - DynamicContext context = new DynamicContext(configuration, null); - rootSqlNode.apply(context); - return context.getSql(); + List parameterMappings = new ArrayList<>(); + ParameterMappingTokenHandler tokenHandler = new ParameterMappingTokenHandler(parameterMappings, configuration, + clazz, new HashMap<>()); + GenericTokenParser parser = new GenericTokenParser("#{", "}", tokenHandler); + sqlSource = SqlSourceBuilder.buildSqlSource(configuration, parser.parse(sql), parameterMappings); } @Override diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java index 8cf89f4d862..195f9b2cfbb 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2024 the original author or authors. + * Copyright 2009-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,9 @@ */ package org.apache.ibatis.scripting.xmltags; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.StringJoiner; @@ -23,6 +25,9 @@ import ognl.OgnlRuntime; import ognl.PropertyAccessor; +import org.apache.ibatis.builder.ParameterMappingTokenHandler; +import org.apache.ibatis.mapping.ParameterMapping; +import org.apache.ibatis.parsing.GenericTokenParser; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.session.Configuration; @@ -38,20 +43,36 @@ public class DynamicContext { OgnlRuntime.setPropertyAccessor(ContextMap.class, new ContextAccessor()); } - private final ContextMap bindings; + protected final ContextMap bindings; private final StringJoiner sqlBuilder = new StringJoiner(" "); - private int uniqueNumber; - public DynamicContext(Configuration configuration, Object parameterObject) { - if (parameterObject != null && !(parameterObject instanceof Map)) { + private final Configuration configuration; + private final Object parameterObject; + private final Class parameterType; + private final boolean paramExists; + + private GenericTokenParser tokenParser; + private ParameterMappingTokenHandler tokenHandler; + + public DynamicContext(Configuration configuration, Class parameterType) { + this(configuration, null, parameterType, false); + } + + public DynamicContext(Configuration configuration, Object parameterObject, Class parameterType, + boolean paramExists) { + if (parameterObject == null || parameterObject instanceof Map) { + bindings = new ContextMap(null, false); + } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); boolean existsTypeHandler = configuration.getTypeHandlerRegistry().hasTypeHandler(parameterObject.getClass()); bindings = new ContextMap(metaObject, existsTypeHandler); - } else { - bindings = new ContextMap(null, false); } bindings.put(PARAMETER_OBJECT_KEY, parameterObject); bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId()); + this.configuration = configuration; + this.parameterObject = parameterObject; + this.paramExists = paramExists; + this.parameterType = parameterType; } public Map getBindings() { @@ -70,8 +91,34 @@ public String getSql() { return sqlBuilder.toString().trim(); } - public int getUniqueNumber() { - return uniqueNumber++; + private void initTokenParser(List parameterMappings) { + if (tokenParser == null) { + tokenHandler = new ParameterMappingTokenHandler(parameterMappings != null ? parameterMappings : new ArrayList<>(), + configuration, parameterObject, parameterType, bindings, paramExists); + tokenParser = new GenericTokenParser("#{", "}", tokenHandler); + } + } + + public List getParameterMappings() { + initTokenParser(null); + return tokenHandler.getParameterMappings(); + } + + protected String parseParam(String sql) { + initTokenParser(getParameterMappings()); + return tokenParser.parse(sql); + } + + protected Object getParameterObject() { + return parameterObject; + } + + protected Class getParameterType() { + return parameterType; + } + + protected boolean isParamExists() { + return paramExists; } static class ContextMap extends HashMap { diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java index fce0bfa5396..03ab703b2b7 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/DynamicSqlSource.java @@ -35,11 +35,10 @@ public DynamicSqlSource(Configuration configuration, SqlNode rootSqlNode) { @Override public BoundSql getBoundSql(Object parameterObject) { - DynamicContext context = new DynamicContext(configuration, parameterObject); + DynamicContext context = new DynamicContext(configuration, parameterObject, null, true); rootSqlNode.apply(context); - SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); - Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); - SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); + String sql = context.getSql(); + SqlSource sqlSource = SqlSourceBuilder.buildSqlSource(configuration, sql, context.getParameterMappings()); BoundSql boundSql = sqlSource.getBoundSql(parameterObject); context.getBindings().forEach(boundSql::setAdditionalParameter); return boundSql; diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java b/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java index 2111f2f4a08..0c843faa284 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNode.java @@ -15,17 +15,17 @@ */ package org.apache.ibatis.scripting.xmltags; +import java.util.List; import java.util.Map; import java.util.Optional; -import org.apache.ibatis.parsing.GenericTokenParser; +import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.session.Configuration; /** * @author Clinton Begin */ public class ForEachSqlNode implements SqlNode { - public static final String ITEM_PREFIX = "__frch_"; private final ExpressionEvaluator evaluator = ExpressionEvaluator.INSTANCE; private final String collectionExpression; @@ -76,47 +76,41 @@ public boolean apply(DynamicContext context) { applyOpen(context); int i = 0; for (Object o : iterable) { - DynamicContext oldContext = context; + DynamicContext scopedContext; if (first || separator == null) { - context = new PrefixedContext(context, ""); + scopedContext = new PrefixedContext(context, ""); } else { - context = new PrefixedContext(context, separator); + scopedContext = new PrefixedContext(context, separator); } - int uniqueNumber = context.getUniqueNumber(); // Issue #709 if (o instanceof Map.Entry) { @SuppressWarnings("unchecked") Map.Entry mapEntry = (Map.Entry) o; - applyIndex(context, mapEntry.getKey(), uniqueNumber); - applyItem(context, mapEntry.getValue(), uniqueNumber); + applyIndex(scopedContext, mapEntry.getKey()); + applyItem(scopedContext, mapEntry.getValue()); } else { - applyIndex(context, i, uniqueNumber); - applyItem(context, o, uniqueNumber); + applyIndex(scopedContext, i); + applyItem(scopedContext, o); } - contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber)); + contents.apply(scopedContext); if (first) { - first = !((PrefixedContext) context).isPrefixApplied(); + first = !((PrefixedContext) scopedContext).isPrefixApplied(); } - context = oldContext; i++; } applyClose(context); - context.getBindings().remove(item); - context.getBindings().remove(index); return true; } - private void applyIndex(DynamicContext context, Object o, int i) { + private void applyIndex(DynamicContext context, Object o) { if (index != null) { context.bind(index, o); - context.bind(itemizeItem(index, i), o); } } - private void applyItem(DynamicContext context, Object o, int i) { + private void applyItem(DynamicContext context, Object o) { if (item != null) { context.bind(item, o); - context.bind(itemizeItem(item, i), o); } } @@ -132,86 +126,23 @@ private void applyClose(DynamicContext context) { } } - private static String itemizeItem(String item, int i) { - return ITEM_PREFIX + item + "_" + i; - } - - private static class FilteredDynamicContext extends DynamicContext { - private final DynamicContext delegate; - private final int index; - private final String itemIndex; - private final String item; - - public FilteredDynamicContext(Configuration configuration, DynamicContext delegate, String itemIndex, String item, - int i) { - super(configuration, null); - this.delegate = delegate; - this.index = i; - this.itemIndex = itemIndex; - this.item = item; - } - - @Override - public Map getBindings() { - return delegate.getBindings(); - } - - @Override - public void bind(String name, Object value) { - delegate.bind(name, value); - } - - @Override - public String getSql() { - return delegate.getSql(); - } - - @Override - public void appendSql(String sql) { - GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> { - String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index)); - if (itemIndex != null && newContent.equals(content)) { - newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index)); - } - return "#{" + newContent + "}"; - }); - - delegate.appendSql(parser.parse(sql)); - } - - @Override - public int getUniqueNumber() { - return delegate.getUniqueNumber(); - } - - } - private class PrefixedContext extends DynamicContext { private final DynamicContext delegate; private final String prefix; private boolean prefixApplied; public PrefixedContext(DynamicContext delegate, String prefix) { - super(configuration, null); + super(configuration, delegate.getParameterObject(), delegate.getParameterType(), delegate.isParamExists()); this.delegate = delegate; this.prefix = prefix; this.prefixApplied = false; + this.bindings.putAll(delegate.getBindings()); } public boolean isPrefixApplied() { return prefixApplied; } - @Override - public Map getBindings() { - return delegate.getBindings(); - } - - @Override - public void bind(String name, Object value) { - delegate.bind(name, value); - } - @Override public void appendSql(String sql) { if (!prefixApplied && sql != null && sql.trim().length() > 0) { @@ -227,8 +158,8 @@ public String getSql() { } @Override - public int getUniqueNumber() { - return delegate.getUniqueNumber(); + public List getParameterMappings() { + return delegate.getParameterMappings(); } } diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/StaticTextSqlNode.java b/src/main/java/org/apache/ibatis/scripting/xmltags/StaticTextSqlNode.java index 896d837f76d..af42ce8c64b 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/StaticTextSqlNode.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/StaticTextSqlNode.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2023 the original author or authors. + * Copyright 2009-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ public StaticTextSqlNode(String text) { @Override public boolean apply(DynamicContext context) { - context.appendSql(text); + context.appendSql(context.parseParam(text)); return true; } diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/TextSqlNode.java b/src/main/java/org/apache/ibatis/scripting/xmltags/TextSqlNode.java index 4bcb6af1d62..e760f4408ad 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/TextSqlNode.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/TextSqlNode.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2024 the original author or authors. + * Copyright 2009-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,8 @@ */ package org.apache.ibatis.scripting.xmltags; -import java.util.regex.Pattern; - import org.apache.ibatis.parsing.GenericTokenParser; import org.apache.ibatis.parsing.TokenHandler; -import org.apache.ibatis.scripting.ScriptingException; import org.apache.ibatis.type.SimpleTypeRegistry; /** @@ -27,15 +24,9 @@ */ public class TextSqlNode implements SqlNode { private final String text; - private final Pattern injectionFilter; public TextSqlNode(String text) { - this(text, null); - } - - public TextSqlNode(String text, Pattern injectionFilter) { this.text = text; - this.injectionFilter = injectionFilter; } public boolean isDynamic() { @@ -47,8 +38,8 @@ public boolean isDynamic() { @Override public boolean apply(DynamicContext context) { - GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter)); - context.appendSql(parser.parse(text)); + GenericTokenParser parser = createParser(new BindingTokenParser(context)); + context.appendSql(context.parseParam(parser.parse(text))); return true; } @@ -59,11 +50,9 @@ private GenericTokenParser createParser(TokenHandler handler) { private static class BindingTokenParser implements TokenHandler { private final DynamicContext context; - private final Pattern injectionFilter; - public BindingTokenParser(DynamicContext context, Pattern injectionFilter) { + public BindingTokenParser(DynamicContext context) { this.context = context; - this.injectionFilter = injectionFilter; } @Override @@ -75,15 +64,8 @@ public String handleToken(String content) { context.getBindings().put("value", parameter); } Object value = OgnlCache.getValue(content, context.getBindings()); - String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null" - checkInjection(srtValue); - return srtValue; - } - - private void checkInjection(String value) { - if (injectionFilter != null && !injectionFilter.matcher(value).matches()) { - throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern()); - } + // issue #274 return "" instead of "null" + return value == null ? "" : String.valueOf(value); } } diff --git a/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java b/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java index 95c78db85d8..1a1053794ec 100644 --- a/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java +++ b/src/main/java/org/apache/ibatis/scripting/xmltags/TrimSqlNode.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2023 the original author or authors. + * Copyright 2009-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,9 +19,9 @@ import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.StringTokenizer; +import org.apache.ibatis.mapping.ParameterMapping; import org.apache.ibatis.session.Configuration; /** @@ -79,11 +79,12 @@ private class FilteredDynamicContext extends DynamicContext { private StringBuilder sqlBuffer; public FilteredDynamicContext(DynamicContext delegate) { - super(configuration, null); + super(configuration, delegate.getParameterObject(), delegate.getParameterType(), delegate.isParamExists()); this.delegate = delegate; this.prefixApplied = false; this.suffixApplied = false; this.sqlBuffer = new StringBuilder(); + this.bindings.putAll(delegate.getBindings()); } public void applyAll() { @@ -96,21 +97,6 @@ public void applyAll() { delegate.appendSql(sqlBuffer.toString()); } - @Override - public Map getBindings() { - return delegate.getBindings(); - } - - @Override - public void bind(String name, Object value) { - delegate.bind(name, value); - } - - @Override - public int getUniqueNumber() { - return delegate.getUniqueNumber(); - } - @Override public void appendSql(String sql) { sqlBuffer.append(sql); @@ -121,6 +107,11 @@ public String getSql() { return delegate.getSql(); } + @Override + public List getParameterMappings() { + return delegate.getParameterMappings(); + } + private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) { if (prefixApplied) { return; diff --git a/src/test/java/org/apache/ibatis/builder/SqlSourceBuilderTest.java b/src/test/java/org/apache/ibatis/builder/SqlSourceBuilderTest.java index 34c0dcb9b3e..5d2b8ea5f3f 100644 --- a/src/test/java/org/apache/ibatis/builder/SqlSourceBuilderTest.java +++ b/src/test/java/org/apache/ibatis/builder/SqlSourceBuilderTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2024 the original author or authors. + * Copyright 2009-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,41 +15,16 @@ */ package org.apache.ibatis.builder; -import org.apache.ibatis.mapping.BoundSql; -import org.apache.ibatis.mapping.SqlSource; -import org.apache.ibatis.session.Configuration; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class SqlSourceBuilderTest { - private static Configuration configuration; - private static SqlSourceBuilder sqlSourceBuilder; private final String sqlFromXml = "\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t "; - @BeforeEach - void setUp() { - configuration = new Configuration(); - - sqlSourceBuilder = new SqlSourceBuilder(configuration); - } - - @Test - void shrinkWhitespacesInSqlIsFalse() { - SqlSource sqlSource = sqlSourceBuilder.parse(sqlFromXml, null, null); - BoundSql boundSql = sqlSource.getBoundSql(null); - String actual = boundSql.getSql(); - Assertions.assertEquals(sqlFromXml, actual); - } - @Test void shrinkWhitespacesInSqlIsTrue() { - configuration.setShrinkWhitespacesInSql(true); - SqlSource sqlSource = sqlSourceBuilder.parse(sqlFromXml, null, null); - BoundSql boundSql = sqlSource.getBoundSql(null); - String actual = boundSql.getSql(); - + String actual = SqlSourceBuilder.removeExtraWhitespaces(sqlFromXml); String shrankWhitespacesInSql = "SELECT * FROM user WHERE user_id = 1"; Assertions.assertEquals(shrankWhitespacesInSql, actual); } diff --git a/src/test/java/org/apache/ibatis/builder/xml/dynamic/DynamicSqlSourceTest.java b/src/test/java/org/apache/ibatis/builder/xml/dynamic/DynamicSqlSourceTest.java index 13e85dd9bd0..e66ddd15a2c 100644 --- a/src/test/java/org/apache/ibatis/builder/xml/dynamic/DynamicSqlSourceTest.java +++ b/src/test/java/org/apache/ibatis/builder/xml/dynamic/DynamicSqlSourceTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2024 the original author or authors. + * Copyright 2009-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Stream; import org.apache.ibatis.BaseDataTest; import org.apache.ibatis.io.Resources; @@ -36,6 +37,7 @@ import org.apache.ibatis.scripting.xmltags.MixedSqlNode; import org.apache.ibatis.scripting.xmltags.SetSqlNode; import org.apache.ibatis.scripting.xmltags.SqlNode; +import org.apache.ibatis.scripting.xmltags.StaticTextSqlNode; import org.apache.ibatis.scripting.xmltags.TextSqlNode; import org.apache.ibatis.scripting.xmltags.WhereSqlNode; import org.apache.ibatis.session.Configuration; @@ -43,6 +45,9 @@ import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class DynamicSqlSourceTest extends BaseDataTest { @@ -277,9 +282,9 @@ void shouldIterateOnceForEachItemInCollection() throws Exception { BoundSql boundSql = source.getBoundSql(parameterObject); assertEquals(expected, boundSql.getSql()); assertEquals(3, boundSql.getParameterMappings().size()); - assertEquals("__frch_item_0", boundSql.getParameterMappings().get(0).getProperty()); - assertEquals("__frch_item_1", boundSql.getParameterMappings().get(1).getProperty()); - assertEquals("__frch_item_2", boundSql.getParameterMappings().get(2).getProperty()); + assertEquals("item", boundSql.getParameterMappings().get(0).getProperty()); + assertEquals("item", boundSql.getParameterMappings().get(1).getProperty()); + assertEquals("item", boundSql.getParameterMappings().get(2).getProperty()); } @Test @@ -333,9 +338,24 @@ void shouldPerformStrictMatchOnForEachVariableSubstitution() throws Exception { BoundSql boundSql = source.getBoundSql(param); assertEquals(4, boundSql.getParameterMappings().size()); assertEquals("uuu.u", boundSql.getParameterMappings().get(0).getProperty()); - assertEquals("__frch_u_0.id", boundSql.getParameterMappings().get(1).getProperty()); - assertEquals("__frch_u_0", boundSql.getParameterMappings().get(2).getProperty()); - assertEquals("__frch_u_0", boundSql.getParameterMappings().get(3).getProperty()); + assertEquals("u.id", boundSql.getParameterMappings().get(1).getProperty()); + assertEquals("u", boundSql.getParameterMappings().get(2).getProperty()); + assertEquals("u", boundSql.getParameterMappings().get(3).getProperty()); + } + + @Test + void cornerCase_ForeachComesFirst() throws Exception { + final Map param = new HashMap<>(); + List beans = new ArrayList<>(); + beans.add(new Bean("bean id 1")); + beans.add(new Bean("bean id 2")); + param.put("beans", beans); + DynamicSqlSource source = createDynamicSqlSource(new ForEachSqlNode(new Configuration(), + mixedContents(new TextSqlNode("#{b.id}")), "beans", false, null, "b", "(", ")", ",")); + BoundSql boundSql = source.getBoundSql(param); + assertEquals(2, boundSql.getParameterMappings().size()); + assertEquals("b.id", boundSql.getParameterMappings().get(0).getProperty()); + assertEquals("b.id", boundSql.getParameterMappings().get(1).getProperty()); } private DynamicSqlSource createDynamicSqlSource(SqlNode... contents) throws IOException, SQLException { @@ -377,4 +397,21 @@ public void setId(String property) { } } + @MethodSource + @ParameterizedTest + void testShrinkWhitespacesInSql(SqlNode input, boolean shrinkWhitespaces, String expected) { + Configuration config = new Configuration(); + config.setShrinkWhitespacesInSql(shrinkWhitespaces); + String actual = new DynamicSqlSource(config, input).getBoundSql(null).getSql(); + assertEquals(expected, actual); + } + + static Stream testShrinkWhitespacesInSql() { + return Stream.of( + Arguments.arguments( + new StaticTextSqlNode("\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t "), false, + "SELECT * \n FROM user\n \t WHERE user_id = 1"), + Arguments.arguments(new StaticTextSqlNode("\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t"), true, + "SELECT * FROM user WHERE user_id = 1")); + } } diff --git a/src/test/java/org/apache/ibatis/scripting/xmltags/ChooseSqlNodeTest.java b/src/test/java/org/apache/ibatis/scripting/xmltags/ChooseSqlNodeTest.java deleted file mode 100644 index 73e8e4e7bed..00000000000 --- a/src/test/java/org/apache/ibatis/scripting/xmltags/ChooseSqlNodeTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2009-2024 the original author or authors. - * - * Licensed 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 - * - * https://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.ibatis.scripting.xmltags; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; - -import org.apache.ibatis.domain.blog.Author; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - *
{@code
- *  SELECT *
- *  FROM BLOG
- *  WHERE state = 'active'
- *  
- * 		
- * 		 	AND title like #{title}
- * 		
- * 		
- * 		 	AND author_name like #{author.username}
- * 		
- * 		
- * 		 	AND featured = 1
- * 		
- *  
- * }
- * - * @author mawen12 - * - * @see choose - */ -class ChooseSqlNodeTest extends SqlNodeBase { - - private static final String FIRST_TEXT = " AND title like #{title}"; - private static final String SECOND_TEXT = " AND author_name like #{author.username}"; - private static final String OTHERWISE_TEXT = " AND featured = 1"; - - private SqlNode sqlNode; - - @BeforeEach - void setup() { - SqlNode first = new IfSqlNode(new StaticTextSqlNode(FIRST_TEXT), "title != null"); - SqlNode second = new IfSqlNode(new StaticTextSqlNode(SECOND_TEXT), "author != null && author.username != null"); - List ifNodes = Arrays.asList(first, second); - - SqlNode defaultNode = new StaticTextSqlNode(OTHERWISE_TEXT); - - this.sqlNode = new ChooseSqlNode(ifNodes, defaultNode); - } - - @Test - @Override - public void shouldApply() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>() { - private static final long serialVersionUID = 1L; - - { - put("title", "abc"); - put("author", new Author(1, "mybatis", "***", null, null, null)); - } - }); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql(FIRST_TEXT); - } - - @Test - void shouldAppendSecond() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>() { - private static final long serialVersionUID = 1L; - - { - put("author", new Author(1, "mybatis", "***", null, null, null)); - } - }); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql(SECOND_TEXT); - } - - @Test - void shouldAppendOtherwise() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>()); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql(OTHERWISE_TEXT); - } -} diff --git a/src/test/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNodeTest.java b/src/test/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNodeTest.java deleted file mode 100644 index 7a551bc0a8d..00000000000 --- a/src/test/java/org/apache/ibatis/scripting/xmltags/ForEachSqlNodeTest.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2009-2024 the original author or authors. - * - * Licensed 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 - * - * https://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.ibatis.scripting.xmltags; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; - -/** - *
{@code
- * 	SELECT *
- * 	FROM POST
- * 	
- * 	    
- * 	        #{item}
- * 	    
- * 	
- * }
- * - * @author mawen12 - * - * @see foreach - */ -class ForEachSqlNodeTest extends SqlNodeBase { - - private SqlNode sqlNode; - - @BeforeEach - void setup() { - SqlNode contents = new StaticTextSqlNode("#{name}"); - this.sqlNode = new ForEachSqlNode(configuration, contents, "list", "index", "item", "ID in (", ")", ","); - } - - @Test - @Override - public void shouldApply() throws Exception { - ArgumentCaptor bindKeyCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor bindValueCaptor = ArgumentCaptor.forClass(Object.class); - doNothing().when(context).bind(bindKeyCaptor.capture(), bindValueCaptor.capture()); - - when(context.getBindings()).thenReturn(new HashMap<>() { - private static final long serialVersionUID = 1L; - - { - put("list", Arrays.asList("a", "b", "c")); - } - }); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql("ID in ("); - verify(context).appendSql(")"); - - List allKeyValues = bindKeyCaptor.getAllValues(); - List allValValues = bindValueCaptor.getAllValues(); - assertEquals(Arrays.asList("index", "__frch_index_0", "item", "__frch_item_0", "index", "__frch_index_0", "item", - "__frch_item_0", "index", "__frch_index_0", "item", "__frch_item_0"), allKeyValues); - assertEquals(Arrays.asList(0, 0, "a", "a", 1, 1, "b", "b", 2, 2, "c", "c"), allValValues); - } -} diff --git a/src/test/java/org/apache/ibatis/scripting/xmltags/IfSqlNodeTest.java b/src/test/java/org/apache/ibatis/scripting/xmltags/IfSqlNodeTest.java deleted file mode 100644 index 71e2b460479..00000000000 --- a/src/test/java/org/apache/ibatis/scripting/xmltags/IfSqlNodeTest.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2009-2024 the original author or authors. - * - * Licensed 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 - * - * https://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.ibatis.scripting.xmltags; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.HashMap; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - *
{@code
- * 	mawen12
- *
- * @see if
- */
-class IfSqlNodeTest extends SqlNodeBase {
-
-  private static final String CONDITION = "title != null";
-  private static final String TEXT = "AND title like #{title}";
-
-  private SqlNode sqlNode;
-
-  @BeforeEach
-  void setup() {
-    SqlNode contents = new StaticTextSqlNode(TEXT);
-    this.sqlNode = new IfSqlNode(contents, CONDITION);
-  }
-
-  @Test
-  @Override
-  public void shouldApply() throws Exception {
-    when(context.getBindings()).thenReturn(new HashMap<>() {
-      private static final long serialVersionUID = 1L;
-
-      {
-        put("title", "ENGLISH");
-      }
-    });
-
-    boolean result = sqlNode.apply(context);
-
-    assertTrue(result);
-    verify(context).appendSql(TEXT);
-  }
-
-  @Test
-  void shouldAppendNone() {
-    when(context.getBindings()).thenReturn(new HashMap<>() {
-      private static final long serialVersionUID = 1L;
-
-      {
-        put("title", null);
-      }
-    });
-
-    boolean result = sqlNode.apply(context);
-
-    assertFalse(result);
-    verify(context, never()).appendSql(TEXT);
-  }
-}
diff --git a/src/test/java/org/apache/ibatis/scripting/xmltags/MixedSqlNodeTest.java b/src/test/java/org/apache/ibatis/scripting/xmltags/MixedSqlNodeTest.java
deleted file mode 100644
index 63c98cc1126..00000000000
--- a/src/test/java/org/apache/ibatis/scripting/xmltags/MixedSqlNodeTest.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- *    Copyright 2009-2024 the original author or authors.
- *
- *    Licensed 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
- *
- *       https://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.ibatis.scripting.xmltags;
-
-import static org.mockito.Mockito.verify;
-
-import java.util.Arrays;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-/**
- * @author mawen12
- */
-class MixedSqlNodeTest extends SqlNodeBase {
-
-  private static final String FIRST_TEXT = "abc";
-  private static final String SECOND_TEXT = "bcd";
-  private SqlNode sqlNode;
-
-  @BeforeEach
-  void setup() {
-    SqlNode first = new StaticTextSqlNode(FIRST_TEXT);
-    SqlNode second = new StaticTextSqlNode(SECOND_TEXT);
-    this.sqlNode = new MixedSqlNode(Arrays.asList(first, second));
-  }
-
-  @Test
-  @Override
-  public void shouldApply() throws Exception {
-    sqlNode.apply(context);
-
-    verify(context).appendSql("abc");
-    verify(context).appendSql("bcd");
-  }
-}
diff --git a/src/test/java/org/apache/ibatis/scripting/xmltags/SetSqlNodeTest.java b/src/test/java/org/apache/ibatis/scripting/xmltags/SetSqlNodeTest.java
deleted file mode 100644
index e09c32709ba..00000000000
--- a/src/test/java/org/apache/ibatis/scripting/xmltags/SetSqlNodeTest.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- *    Copyright 2009-2024 the original author or authors.
- *
- *    Licensed 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
- *
- *       https://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.ibatis.scripting.xmltags;
-
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.util.Arrays;
-import java.util.HashMap;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-/**
- * 
{@code
- * 	UPDATE author
- * 	
- * 		
- * 		 	password = #{password}
- * 		
- * 	
- * 	WHERE id = #{id}
- * }
- * - * @author mawen12 - * - * @see trim-where-set - */ -class SetSqlNodeTest extends SqlNodeBase { - - private static final String FIRST_TEXT = " username = #{username},"; - private static final String SECOND_TEXT = " password = #{password}"; - - private SqlNode sqlNode; - - @BeforeEach - void setup() { - SqlNode first = new IfSqlNode(new StaticTextSqlNode(FIRST_TEXT), "username != null"); - SqlNode second = new IfSqlNode(new StaticTextSqlNode(SECOND_TEXT), "password != null"); - SqlNode contents = new MixedSqlNode(Arrays.asList(first, second)); - - this.sqlNode = new SetSqlNode(configuration, contents); - } - - @Test - @Override - public void shouldApply() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>() { - private static final long serialVersionUID = 1L; - - { - put("username", "Jack"); - put("password", "***"); - } - }); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql("SET username = #{username}, password = #{password}"); - } - - @Test - void shouldAppendOnlyUsername() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>() { - private static final long serialVersionUID = 1L; - - { - put("username", "Jack"); - } - }); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql("SET username = #{username}"); - } - - @Test - void shouldAppendOnlyPassword() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>() { - private static final long serialVersionUID = 1L; - - { - put("password", "***"); - } - }); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql("SET password = #{password}"); - } - - @Test - void shouldAppendNone() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>()); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql(""); - } -} diff --git a/src/test/java/org/apache/ibatis/scripting/xmltags/StaticTextSqlNodeTest.java b/src/test/java/org/apache/ibatis/scripting/xmltags/StaticTextSqlNodeTest.java deleted file mode 100644 index 5f15c888f43..00000000000 --- a/src/test/java/org/apache/ibatis/scripting/xmltags/StaticTextSqlNodeTest.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2009-2024 the original author or authors. - * - * Licensed 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 - * - * https://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.ibatis.scripting.xmltags; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.verify; - -import org.junit.jupiter.api.Test; - -/** - * @author mawen12 - */ -class StaticTextSqlNodeTest extends SqlNodeBase { - - private static final String TEXT = "select 1 from dual"; - - @Test - @Override - public void shouldApply() throws Exception { - // given - SqlNode sqlNode = new StaticTextSqlNode(TEXT); - - // when - boolean result = sqlNode.apply(context); - - // then - assertTrue(result); - verify(context).appendSql(TEXT); - } -} diff --git a/src/test/java/org/apache/ibatis/scripting/xmltags/TextSqlNodeTest.java b/src/test/java/org/apache/ibatis/scripting/xmltags/TextSqlNodeTest.java deleted file mode 100644 index 0cbed0b8895..00000000000 --- a/src/test/java/org/apache/ibatis/scripting/xmltags/TextSqlNodeTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2009-2024 the original author or authors. - * - * Licensed 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 - * - * https://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.ibatis.scripting.xmltags; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.HashMap; - -import org.junit.jupiter.api.Test; - -/** - * @author mawen12 - */ -class TextSqlNodeTest extends SqlNodeBase { - - private static final String TEXT = "select 1 from dual"; - private static final String DYNAMIC_TEXT = "select * from user where id = ${id}"; - - @Test - @Override - public void shouldApply() throws Exception { - // given - TextSqlNode sqlNode = new TextSqlNode(TEXT); - - // when - boolean result = sqlNode.apply(context); - - // then - assertTrue(result); - assertFalse(sqlNode.isDynamic()); - verify(context).appendSql(TEXT); - } - - @Test - void shouldApplyDynamic() { - // given - TextSqlNode sqlNode = new TextSqlNode(DYNAMIC_TEXT); - when(context.getBindings()).thenReturn(new HashMap<>() { - private static final long serialVersionUID = 1L; - - { - put("id", 1); - } - }); - - // when - boolean result = sqlNode.apply(context); - - // then - assertTrue(result); - assertTrue(sqlNode.isDynamic()); - verify(context).appendSql("select * from user where id = 1"); - } -} diff --git a/src/test/java/org/apache/ibatis/scripting/xmltags/TrimSqlNodeTest.java b/src/test/java/org/apache/ibatis/scripting/xmltags/TrimSqlNodeTest.java deleted file mode 100644 index a47172f33e3..00000000000 --- a/src/test/java/org/apache/ibatis/scripting/xmltags/TrimSqlNodeTest.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2009-2024 the original author or authors. - * - * Licensed 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 - * - * https://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.ibatis.scripting.xmltags; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Arrays; -import java.util.HashMap; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - *
{@code
- * 	SELECT *
- * 	FROM users
- * 	
- * 	    
- * 			AND id = #{id}
- * 	    
- * 	    
- * 			AND name = #{name}
- * 	    
- * 	
- * }
- * - * @author mawen12 - * - * @see trim-where-set - */ -class TrimSqlNodeTest extends SqlNodeBase { - - private static final String FIRST_TEXT = " AND id = #{id}"; - private static final String SECOND_TEXT = " AND name = #{name}"; - private static final String PREFIX = "WHERE"; - private static final String PREFIX_OVERRIDES = "AND |OR "; - - private SqlNode sqlNode; - - @BeforeEach - void setup() { - SqlNode first = new IfSqlNode(new StaticTextSqlNode(FIRST_TEXT), "id != null"); - SqlNode second = new IfSqlNode(new StaticTextSqlNode(SECOND_TEXT), "name != null"); - SqlNode contents = new MixedSqlNode(Arrays.asList(first, second)); - - this.sqlNode = new TrimSqlNode(configuration, contents, PREFIX, PREFIX_OVERRIDES, null, null); - } - - @Test - @Override - public void shouldApply() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>() { - private static final long serialVersionUID = 1L; - - { - put("id", 1); - put("name", "mybatis"); - } - }); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql("WHERE id = #{id} AND name = #{name}"); - } - - @Test - void shouldAppendOnlyId() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>() { - private static final long serialVersionUID = 1L; - - { - put("id", 1); - } - }); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql("WHERE id = #{id}"); - } - - @Test - void shouldAppendOnlyName() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>() { - private static final long serialVersionUID = 1L; - - { - put("name", "mybatis"); - } - }); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql("WHERE name = #{name}"); - } - - @Test - void shouldAppendNone() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>()); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql(""); - } -} diff --git a/src/test/java/org/apache/ibatis/scripting/xmltags/VarDeclSqlNodeTest.java b/src/test/java/org/apache/ibatis/scripting/xmltags/VarDeclSqlNodeTest.java deleted file mode 100644 index 8e58f5b5bb9..00000000000 --- a/src/test/java/org/apache/ibatis/scripting/xmltags/VarDeclSqlNodeTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2009-2024 the original author or authors. - * - * Licensed 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 - * - * https://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.ibatis.scripting.xmltags; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.HashMap; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - *
{@code
- * 	
- * 	SELECT * FROM BLOG
- * 	WHERE title like #{pattern}
- * }
- * - * @author mawen12 - * - * @see bind - */ -class VarDeclSqlNodeTest extends SqlNodeBase { - - private SqlNode sqlNode; - - @BeforeEach - void setup() { - this.sqlNode = new VarDeclSqlNode("pattern", "'%' + _parameter.getTitle() + '%'"); - } - - @Test - @Override - public void shouldApply() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>() { - private static final long serialVersionUID = 1L; - - { - put("_parameter", new Bean("abc")); - } - }); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).bind("pattern", "%abc%"); - } - - private static class Bean { - - private String title; - - public Bean(String title) { - this.title = title; - } - - public String getTitle() { - return title; - } - } -} diff --git a/src/test/java/org/apache/ibatis/scripting/xmltags/WhereSqlNodeTest.java b/src/test/java/org/apache/ibatis/scripting/xmltags/WhereSqlNodeTest.java deleted file mode 100644 index d0818ff336b..00000000000 --- a/src/test/java/org/apache/ibatis/scripting/xmltags/WhereSqlNodeTest.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2009-2024 the original author or authors. - * - * Licensed 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 - * - * https://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.ibatis.scripting.xmltags; - -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Arrays; -import java.util.HashMap; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - *
{@code
- * 	SELECT *
- * 	FROM users
- * 	
- * 	    
- * 			AND id = #{id}
- * 	    
- * 	    
- * 			AND name = #{name}
- * 	    
- * 	
- * }
- * - * @author mawen12 - * - * @see trim-where-set - */ -class WhereSqlNodeTest extends SqlNodeBase { - - private static final String FIRST_TEXT = " AND id = #{id}"; - private static final String SECOND_TEXT = " AND name = #{name}"; - - private SqlNode sqlNode; - - @BeforeEach - void setup() { - SqlNode first = new IfSqlNode(new StaticTextSqlNode(FIRST_TEXT), "id != null"); - SqlNode second = new IfSqlNode(new StaticTextSqlNode(SECOND_TEXT), "name != null"); - SqlNode contents = new MixedSqlNode(Arrays.asList(first, second)); - - this.sqlNode = new WhereSqlNode(configuration, contents); - } - - @Test - @Override - public void shouldApply() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>() { - private static final long serialVersionUID = 1L; - - { - put("id", 1); - put("name", "mybatis"); - } - }); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql("WHERE id = #{id} AND name = #{name}"); - } - - @Test - void shouldAppendOnlyId() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>() { - private static final long serialVersionUID = 1L; - - { - put("id", 1); - } - }); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql("WHERE id = #{id}"); - } - - @Test - void shouldAppendOnlyName() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>() { - private static final long serialVersionUID = 1L; - - { - put("name", "mybatis"); - } - }); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql("WHERE name = #{name}"); - } - - @Test - void shouldAppendNone() throws Exception { - when(context.getBindings()).thenReturn(new HashMap<>()); - - boolean result = sqlNode.apply(context); - - assertTrue(result); - verify(context).appendSql(""); - } -} diff --git a/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/BindInForeachTest.java b/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/BindInForeachTest.java new file mode 100644 index 00000000000..1cca47711c1 --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/BindInForeachTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2009-2025 the original author or authors. + * + * Licensed 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 + * + * https://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.ibatis.submitted.bind_in_foreach; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.Reader; +import java.util.List; + +import org.apache.ibatis.BaseDataTest; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class BindInForeachTest { + + private static SqlSessionFactory sqlSessionFactory; + + @BeforeAll + static void setUp() throws Exception { + try (Reader reader = Resources + .getResourceAsReader("org/apache/ibatis/submitted/bind_in_foreach/mybatis-config.xml")) { + sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader); + } + BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), + "org/apache/ibatis/submitted/bind_in_foreach/CreateDB.sql"); + } + + @Test + void testBindInForeach() { + try (SqlSession sqlSession = sqlSessionFactory.openSession()) { + Mapper mapper = sqlSession.getMapper(Mapper.class); + assertEquals(3, mapper.createUsers(List.of(2, 4, 6))); + List users = mapper.selectUsers(); + assertEquals(3, users.size()); + assertEquals(1, users.get(0).getId()); + assertEquals("User2", users.get(0).getName()); + assertEquals(2, users.get(1).getId()); + assertEquals("User4", users.get(1).getName()); + assertEquals(3, users.get(2).getId()); + assertEquals("User6", users.get(2).getName()); + } + } + +} diff --git a/src/test/java/org/apache/ibatis/scripting/xmltags/SqlNodeBase.java b/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/Mapper.java similarity index 51% rename from src/test/java/org/apache/ibatis/scripting/xmltags/SqlNodeBase.java rename to src/test/java/org/apache/ibatis/submitted/bind_in_foreach/Mapper.java index 03864e3e07d..fd4d914a1c4 100644 --- a/src/test/java/org/apache/ibatis/scripting/xmltags/SqlNodeBase.java +++ b/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/Mapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2024 the original author or authors. + * Copyright 2009-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,26 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.ibatis.scripting.xmltags; +package org.apache.ibatis.submitted.bind_in_foreach; -import org.apache.ibatis.session.Configuration; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; +import java.util.List; -/** - * @author mawen12 - * - * @see SqlNode - */ -@ExtendWith(MockitoExtension.class) -abstract class SqlNodeBase { +import org.apache.ibatis.annotations.Select; + +public interface Mapper { - @Mock - protected Configuration configuration; + @Select("select * from users order by id") + List selectUsers(); - @Mock - protected DynamicContext context; + int createUsers(List numbers); - public abstract void shouldApply() throws Exception; } diff --git a/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/User.java b/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/User.java new file mode 100644 index 00000000000..765338fb82a --- /dev/null +++ b/src/test/java/org/apache/ibatis/submitted/bind_in_foreach/User.java @@ -0,0 +1,38 @@ +/* + * Copyright 2009-2022 the original author or authors. + * + * Licensed 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 + * + * https://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.ibatis.submitted.bind_in_foreach; + +public class User { + + private Integer id; + private String name; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/test/java/org/apache/ibatis/submitted/raw_sql_source/RawSqlSourceTest.java b/src/test/java/org/apache/ibatis/submitted/raw_sql_source/RawSqlSourceTest.java index 28fa1475d52..bc42b85badf 100644 --- a/src/test/java/org/apache/ibatis/submitted/raw_sql_source/RawSqlSourceTest.java +++ b/src/test/java/org/apache/ibatis/submitted/raw_sql_source/RawSqlSourceTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2024 the original author or authors. + * Copyright 2009-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,19 +15,28 @@ */ package org.apache.ibatis.submitted.raw_sql_source; +import static org.junit.jupiter.api.Assertions.assertEquals; + import java.io.Reader; +import java.util.stream.Stream; import org.apache.ibatis.BaseDataTest; import org.apache.ibatis.io.Resources; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.scripting.defaults.RawSqlSource; import org.apache.ibatis.scripting.xmltags.DynamicSqlSource; +import org.apache.ibatis.scripting.xmltags.SqlNode; +import org.apache.ibatis.scripting.xmltags.StaticTextSqlNode; +import org.apache.ibatis.session.Configuration; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class RawSqlSourceTest { @@ -72,4 +81,38 @@ private void test(String statement, Class sqlSource) { } } + @MethodSource + @ParameterizedTest + void testShrinkWhitespacesInSql(String input, boolean shrinkWhitespaces, String expected) { + Configuration config = new Configuration(); + config.setShrinkWhitespacesInSql(shrinkWhitespaces); + String actual = new RawSqlSource(config, input, null).getBoundSql(null).getSql(); + assertEquals(expected, actual); + } + + static Stream testShrinkWhitespacesInSql() { + return Stream.of( + Arguments.arguments("\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t ", false, + "\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t "), + Arguments.arguments("\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t", true, + "SELECT * FROM user WHERE user_id = 1")); + } + + @MethodSource + @ParameterizedTest + void testShrinkWhitespacesInSql_SqlNode(SqlNode input, boolean shrinkWhitespaces, String expected) { + Configuration config = new Configuration(); + config.setShrinkWhitespacesInSql(shrinkWhitespaces); + String actual = new RawSqlSource(config, input, null).getBoundSql(null).getSql(); + assertEquals(expected, actual); + } + + static Stream testShrinkWhitespacesInSql_SqlNode() { + return Stream.of( + Arguments.arguments( + new StaticTextSqlNode("\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t "), false, + "SELECT * \n FROM user\n \t WHERE user_id = 1"), + Arguments.arguments(new StaticTextSqlNode("\t\n\n SELECT * \n FROM user\n \t WHERE user_id = 1\n\t"), true, + "SELECT * FROM user WHERE user_id = 1")); + } } diff --git a/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/CreateDB.sql b/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/CreateDB.sql new file mode 100644 index 00000000000..23d3d13e50b --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/CreateDB.sql @@ -0,0 +1,22 @@ +-- +-- Copyright 2009-2022 the original author or authors. +-- +-- Licensed 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 +-- +-- https://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. +-- + +drop table users if exists; + +create table users ( + id int, + name varchar(20) +); diff --git a/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/Mapper.xml b/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/Mapper.xml new file mode 100644 index 00000000000..c735771b349 --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/Mapper.xml @@ -0,0 +1,33 @@ + + + + + + + insert into users (id, name) values + + + + (#{id}, #{name}) + + + + diff --git a/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/mybatis-config.xml b/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/mybatis-config.xml new file mode 100644 index 00000000000..b694f3e402c --- /dev/null +++ b/src/test/resources/org/apache/ibatis/submitted/bind_in_foreach/mybatis-config.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + +