diff --git a/pom.xml b/pom.xml
index dc4a1ae..3bbbf23 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,6 +49,19 @@
5.9.2
test
+
+
+ com.zaxxer
+ HikariCP
+ 4.0.3
+ test
+
+
+ org.postgresql
+ postgresql
+ 42.3.8
+ test
+
diff --git a/src/main/java/org/apache/ibatis/jdbc/AbstractSQL.java b/src/main/java/org/apache/ibatis/jdbc/AbstractSQL.java
new file mode 100644
index 0000000..6e031c4
--- /dev/null
+++ b/src/main/java/org/apache/ibatis/jdbc/AbstractSQL.java
@@ -0,0 +1,740 @@
+/*
+ * Copyright 2009-2023 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.jdbc;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Clinton Begin
+ * @author Jeff Butler
+ * @author Adam Gent
+ * @author Kazuki Shimizu
+ */
+public abstract class AbstractSQL {
+
+ private static final String AND = ") \nAND (";
+ private static final String OR = ") \nOR (";
+
+ private final SQLStatement sql = new SQLStatement();
+
+ public abstract T getSelf();
+
+ public T UPDATE(String table) {
+ sql().statementType = SQLStatement.StatementType.UPDATE;
+ sql().tables.add(table);
+ return getSelf();
+ }
+
+ public T SET(String sets) {
+ sql().sets.add(sets);
+ return getSelf();
+ }
+
+ /**
+ * Sets the.
+ *
+ * @param sets
+ * the sets
+ *
+ * @return the t
+ *
+ * @since 3.4.2
+ */
+ public T SET(String... sets) {
+ sql().sets.addAll(Arrays.asList(sets));
+ return getSelf();
+ }
+
+ public T INSERT_INTO(String tableName) {
+ sql().statementType = SQLStatement.StatementType.INSERT;
+ sql().tables.add(tableName);
+ return getSelf();
+ }
+
+ public T VALUES(String columns, String values) {
+ INTO_COLUMNS(columns);
+ INTO_VALUES(values);
+ return getSelf();
+ }
+
+ /**
+ * Into columns.
+ *
+ * @param columns
+ * the columns
+ *
+ * @return the t
+ *
+ * @since 3.4.2
+ */
+ public T INTO_COLUMNS(String... columns) {
+ sql().columns.addAll(Arrays.asList(columns));
+ return getSelf();
+ }
+
+ /**
+ * Into values.
+ *
+ * @param values
+ * the values
+ *
+ * @return the t
+ *
+ * @since 3.4.2
+ */
+ public T INTO_VALUES(String... values) {
+ List list = sql().valuesList.get(sql().valuesList.size() - 1);
+ Collections.addAll(list, values);
+ return getSelf();
+ }
+
+ public T SELECT(String columns) {
+ sql().statementType = SQLStatement.StatementType.SELECT;
+ sql().select.add(columns);
+ return getSelf();
+ }
+
+ /**
+ * Select.
+ *
+ * @param columns
+ * the columns
+ *
+ * @return the t
+ *
+ * @since 3.4.2
+ */
+ public T SELECT(String... columns) {
+ sql().statementType = SQLStatement.StatementType.SELECT;
+ sql().select.addAll(Arrays.asList(columns));
+ return getSelf();
+ }
+
+ public T SELECT_DISTINCT(String columns) {
+ sql().distinct = true;
+ SELECT(columns);
+ return getSelf();
+ }
+
+ /**
+ * Select distinct.
+ *
+ * @param columns
+ * the columns
+ *
+ * @return the t
+ *
+ * @since 3.4.2
+ */
+ public T SELECT_DISTINCT(String... columns) {
+ sql().distinct = true;
+ SELECT(columns);
+ return getSelf();
+ }
+
+ public T DELETE_FROM(String table) {
+ sql().statementType = SQLStatement.StatementType.DELETE;
+ sql().tables.add(table);
+ return getSelf();
+ }
+
+ public T FROM(String table) {
+ sql().tables.add(table);
+ return getSelf();
+ }
+
+ /**
+ * From.
+ *
+ * @param tables
+ * the tables
+ *
+ * @return the t
+ *
+ * @since 3.4.2
+ */
+ public T FROM(String... tables) {
+ sql().tables.addAll(Arrays.asList(tables));
+ return getSelf();
+ }
+
+ public T JOIN(String join) {
+ sql().join.add(join);
+ return getSelf();
+ }
+
+ /**
+ * Join.
+ *
+ * @param joins
+ * the joins
+ *
+ * @return the t
+ *
+ * @since 3.4.2
+ */
+ public T JOIN(String... joins) {
+ sql().join.addAll(Arrays.asList(joins));
+ return getSelf();
+ }
+
+ public T INNER_JOIN(String join) {
+ sql().innerJoin.add(join);
+ return getSelf();
+ }
+
+ /**
+ * Inner join.
+ *
+ * @param joins
+ * the joins
+ *
+ * @return the t
+ *
+ * @since 3.4.2
+ */
+ public T INNER_JOIN(String... joins) {
+ sql().innerJoin.addAll(Arrays.asList(joins));
+ return getSelf();
+ }
+
+ public T LEFT_OUTER_JOIN(String join) {
+ sql().leftOuterJoin.add(join);
+ return getSelf();
+ }
+
+ /**
+ * Left outer join.
+ *
+ * @param joins
+ * the joins
+ *
+ * @return the t
+ *
+ * @since 3.4.2
+ */
+ public T LEFT_OUTER_JOIN(String... joins) {
+ sql().leftOuterJoin.addAll(Arrays.asList(joins));
+ return getSelf();
+ }
+
+ public T RIGHT_OUTER_JOIN(String join) {
+ sql().rightOuterJoin.add(join);
+ return getSelf();
+ }
+
+ /**
+ * Right outer join.
+ *
+ * @param joins
+ * the joins
+ *
+ * @return the t
+ *
+ * @since 3.4.2
+ */
+ public T RIGHT_OUTER_JOIN(String... joins) {
+ sql().rightOuterJoin.addAll(Arrays.asList(joins));
+ return getSelf();
+ }
+
+ public T OUTER_JOIN(String join) {
+ sql().outerJoin.add(join);
+ return getSelf();
+ }
+
+ /**
+ * Outer join.
+ *
+ * @param joins
+ * the joins
+ *
+ * @return the t
+ *
+ * @since 3.4.2
+ */
+ public T OUTER_JOIN(String... joins) {
+ sql().outerJoin.addAll(Arrays.asList(joins));
+ return getSelf();
+ }
+
+ public T WHERE(String conditions) {
+ sql().where.add(conditions);
+ sql().lastList = sql().where;
+ return getSelf();
+ }
+
+ /**
+ * Where.
+ *
+ * @param conditions
+ * the conditions
+ *
+ * @return the t
+ *
+ * @since 3.4.2
+ */
+ public T WHERE(String... conditions) {
+ sql().where.addAll(Arrays.asList(conditions));
+ sql().lastList = sql().where;
+ return getSelf();
+ }
+
+ public T OR() {
+ sql().lastList.add(OR);
+ return getSelf();
+ }
+
+ public T AND() {
+ sql().lastList.add(AND);
+ return getSelf();
+ }
+
+ public T GROUP_BY(String columns) {
+ sql().groupBy.add(columns);
+ return getSelf();
+ }
+
+ /**
+ * Group by.
+ *
+ * @param columns
+ * the columns
+ *
+ * @return the t
+ *
+ * @since 3.4.2
+ */
+ public T GROUP_BY(String... columns) {
+ sql().groupBy.addAll(Arrays.asList(columns));
+ return getSelf();
+ }
+
+ public T HAVING(String conditions) {
+ sql().having.add(conditions);
+ sql().lastList = sql().having;
+ return getSelf();
+ }
+
+ /**
+ * Having.
+ *
+ * @param conditions
+ * the conditions
+ *
+ * @return the t
+ *
+ * @since 3.4.2
+ */
+ public T HAVING(String... conditions) {
+ sql().having.addAll(Arrays.asList(conditions));
+ sql().lastList = sql().having;
+ return getSelf();
+ }
+
+ public T ORDER_BY(String columns) {
+ sql().orderBy.add(columns);
+ return getSelf();
+ }
+
+ /**
+ * Order by.
+ *
+ * @param columns
+ * the columns
+ *
+ * @return the t
+ *
+ * @since 3.4.2
+ */
+ public T ORDER_BY(String... columns) {
+ sql().orderBy.addAll(Arrays.asList(columns));
+ return getSelf();
+ }
+
+ /**
+ * Set the limit variable string(e.g. {@code "#{limit}"}).
+ *
+ * @param variable
+ * a limit variable string
+ *
+ * @return a self instance
+ *
+ * @see #OFFSET(String)
+ *
+ * @since 3.5.2
+ */
+ public T LIMIT(String variable) {
+ sql().limit = variable;
+ sql().limitingRowsStrategy = SQLStatement.LimitingRowsStrategy.OFFSET_LIMIT;
+ return getSelf();
+ }
+
+ /**
+ * Set the limit value.
+ *
+ * @param value
+ * an offset value
+ *
+ * @return a self instance
+ *
+ * @see #OFFSET(long)
+ *
+ * @since 3.5.2
+ */
+ public T LIMIT(int value) {
+ return LIMIT(String.valueOf(value));
+ }
+
+ /**
+ * Set the offset variable string(e.g. {@code "#{offset}"}).
+ *
+ * @param variable
+ * a offset variable string
+ *
+ * @return a self instance
+ *
+ * @see #LIMIT(String)
+ *
+ * @since 3.5.2
+ */
+ public T OFFSET(String variable) {
+ sql().offset = variable;
+ sql().limitingRowsStrategy = SQLStatement.LimitingRowsStrategy.OFFSET_LIMIT;
+ return getSelf();
+ }
+
+ /**
+ * Set the offset value.
+ *
+ * @param value
+ * an offset value
+ *
+ * @return a self instance
+ *
+ * @see #LIMIT(int)
+ *
+ * @since 3.5.2
+ */
+ public T OFFSET(long value) {
+ return OFFSET(String.valueOf(value));
+ }
+
+ /**
+ * Set the fetch first rows variable string(e.g. {@code "#{fetchFirstRows}"}).
+ *
+ * @param variable
+ * a fetch first rows variable string
+ *
+ * @return a self instance
+ *
+ * @see #OFFSET_ROWS(String)
+ *
+ * @since 3.5.2
+ */
+ public T FETCH_FIRST_ROWS_ONLY(String variable) {
+ sql().limit = variable;
+ sql().limitingRowsStrategy = SQLStatement.LimitingRowsStrategy.ISO;
+ return getSelf();
+ }
+
+ /**
+ * Set the fetch first rows value.
+ *
+ * @param value
+ * a fetch first rows value
+ *
+ * @return a self instance
+ *
+ * @see #OFFSET_ROWS(long)
+ *
+ * @since 3.5.2
+ */
+ public T FETCH_FIRST_ROWS_ONLY(int value) {
+ return FETCH_FIRST_ROWS_ONLY(String.valueOf(value));
+ }
+
+ /**
+ * Set the offset rows variable string(e.g. {@code "#{offset}"}).
+ *
+ * @param variable
+ * a offset rows variable string
+ *
+ * @return a self instance
+ *
+ * @see #FETCH_FIRST_ROWS_ONLY(String)
+ *
+ * @since 3.5.2
+ */
+ public T OFFSET_ROWS(String variable) {
+ sql().offset = variable;
+ sql().limitingRowsStrategy = SQLStatement.LimitingRowsStrategy.ISO;
+ return getSelf();
+ }
+
+ /**
+ * Set the offset rows value.
+ *
+ * @param value
+ * an offset rows value
+ *
+ * @return a self instance
+ *
+ * @see #FETCH_FIRST_ROWS_ONLY(int)
+ *
+ * @since 3.5.2
+ */
+ public T OFFSET_ROWS(long value) {
+ return OFFSET_ROWS(String.valueOf(value));
+ }
+
+ /**
+ * used to add a new inserted row while do multi-row insert.
+ *
+ * @return the t
+ *
+ * @since 3.5.2
+ */
+ public T ADD_ROW() {
+ sql().valuesList.add(new ArrayList<>());
+ return getSelf();
+ }
+
+ private SQLStatement sql() {
+ return sql;
+ }
+
+ public A usingAppender(A a) {
+ sql().sql(a);
+ return a;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sql().sql(sb);
+ return sb.toString();
+ }
+
+ private static class SafeAppendable {
+ private final Appendable appendable;
+ private boolean empty = true;
+
+ public SafeAppendable(Appendable a) {
+ this.appendable = a;
+ }
+
+ public SafeAppendable append(CharSequence s) {
+ try {
+ if (empty && s.length() > 0) {
+ empty = false;
+ }
+ appendable.append(s);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ public boolean isEmpty() {
+ return empty;
+ }
+
+ }
+
+ private static class SQLStatement {
+
+ public enum StatementType {
+
+ DELETE,
+
+ INSERT,
+
+ SELECT,
+
+ UPDATE
+
+ }
+
+ private enum LimitingRowsStrategy {
+ NOP {
+ @Override
+ protected void appendClause(SafeAppendable builder, String offset, String limit) {
+ // NOP
+ }
+ },
+ ISO {
+ @Override
+ protected void appendClause(SafeAppendable builder, String offset, String limit) {
+ if (offset != null) {
+ builder.append(" OFFSET ").append(offset).append(" ROWS");
+ }
+ if (limit != null) {
+ builder.append(" FETCH FIRST ").append(limit).append(" ROWS ONLY");
+ }
+ }
+ },
+ OFFSET_LIMIT {
+ @Override
+ protected void appendClause(SafeAppendable builder, String offset, String limit) {
+ if (limit != null) {
+ builder.append(" LIMIT ").append(limit);
+ }
+ if (offset != null) {
+ builder.append(" OFFSET ").append(offset);
+ }
+ }
+ };
+
+ protected abstract void appendClause(SafeAppendable builder, String offset, String limit);
+
+ }
+
+ StatementType statementType;
+ List sets = new ArrayList<>();
+ List select = new ArrayList<>();
+ List tables = new ArrayList<>();
+ List join = new ArrayList<>();
+ List innerJoin = new ArrayList<>();
+ List outerJoin = new ArrayList<>();
+ List leftOuterJoin = new ArrayList<>();
+ List rightOuterJoin = new ArrayList<>();
+ List where = new ArrayList<>();
+ List having = new ArrayList<>();
+ List groupBy = new ArrayList<>();
+ List orderBy = new ArrayList<>();
+ List lastList = new ArrayList<>();
+ List columns = new ArrayList<>();
+ List> valuesList = new ArrayList<>();
+ boolean distinct;
+ String offset;
+ String limit;
+ LimitingRowsStrategy limitingRowsStrategy = LimitingRowsStrategy.NOP;
+
+ public SQLStatement() {
+ // Prevent Synthetic Access
+ valuesList.add(new ArrayList<>());
+ }
+
+ private void sqlClause(SafeAppendable builder, String keyword, List parts, String open, String close,
+ String conjunction) {
+ if (!parts.isEmpty()) {
+ if (!builder.isEmpty()) {
+ builder.append("\n");
+ }
+ builder.append(keyword);
+ builder.append(" ");
+ builder.append(open);
+ String last = "________";
+ for (int i = 0, n = parts.size(); i < n; i++) {
+ String part = parts.get(i);
+ if (i > 0 && !part.equals(AND) && !part.equals(OR) && !last.equals(AND) && !last.equals(OR)) {
+ builder.append(conjunction);
+ }
+ builder.append(part);
+ last = part;
+ }
+ builder.append(close);
+ }
+ }
+
+ private String selectSQL(SafeAppendable builder) {
+ if (distinct) {
+ sqlClause(builder, "SELECT DISTINCT", select, "", "", ", ");
+ } else {
+ sqlClause(builder, "SELECT", select, "", "", ", ");
+ }
+
+ sqlClause(builder, "FROM", tables, "", "", ", ");
+ joins(builder);
+ sqlClause(builder, "WHERE", where, "(", ")", " AND ");
+ sqlClause(builder, "GROUP BY", groupBy, "", "", ", ");
+ sqlClause(builder, "HAVING", having, "(", ")", " AND ");
+ sqlClause(builder, "ORDER BY", orderBy, "", "", ", ");
+ limitingRowsStrategy.appendClause(builder, offset, limit);
+ return builder.toString();
+ }
+
+ private void joins(SafeAppendable builder) {
+ sqlClause(builder, "JOIN", join, "", "", "\nJOIN ");
+ sqlClause(builder, "INNER JOIN", innerJoin, "", "", "\nINNER JOIN ");
+ sqlClause(builder, "OUTER JOIN", outerJoin, "", "", "\nOUTER JOIN ");
+ sqlClause(builder, "LEFT OUTER JOIN", leftOuterJoin, "", "", "\nLEFT OUTER JOIN ");
+ sqlClause(builder, "RIGHT OUTER JOIN", rightOuterJoin, "", "", "\nRIGHT OUTER JOIN ");
+ }
+
+ private String insertSQL(SafeAppendable builder) {
+ sqlClause(builder, "INSERT INTO", tables, "", "", "");
+ sqlClause(builder, "", columns, "(", ")", ", ");
+ for (int i = 0; i < valuesList.size(); i++) {
+ sqlClause(builder, i > 0 ? "," : "VALUES", valuesList.get(i), "(", ")", ", ");
+ }
+ return builder.toString();
+ }
+
+ private String deleteSQL(SafeAppendable builder) {
+ sqlClause(builder, "DELETE FROM", tables, "", "", "");
+ sqlClause(builder, "WHERE", where, "(", ")", " AND ");
+ limitingRowsStrategy.appendClause(builder, null, limit);
+ return builder.toString();
+ }
+
+ private String updateSQL(SafeAppendable builder) {
+ sqlClause(builder, "UPDATE", tables, "", "", "");
+ joins(builder);
+ sqlClause(builder, "SET", sets, "", "", ", ");
+ sqlClause(builder, "WHERE", where, "(", ")", " AND ");
+ limitingRowsStrategy.appendClause(builder, null, limit);
+ return builder.toString();
+ }
+
+ public String sql(Appendable a) {
+ SafeAppendable builder = new SafeAppendable(a);
+ if (statementType == null) {
+ return null;
+ }
+
+ String answer;
+
+ switch (statementType) {
+ case DELETE:
+ answer = deleteSQL(builder);
+ break;
+
+ case INSERT:
+ answer = insertSQL(builder);
+ break;
+
+ case SELECT:
+ answer = selectSQL(builder);
+ break;
+
+ case UPDATE:
+ answer = updateSQL(builder);
+ break;
+
+ default:
+ answer = null;
+ }
+
+ return answer;
+ }
+ }
+}
diff --git a/src/main/java/xyz/zhouxy/plusone/commons/exception/DbException.java b/src/main/java/xyz/zhouxy/plusone/commons/exception/DbException.java
new file mode 100644
index 0000000..67b3ecb
--- /dev/null
+++ b/src/main/java/xyz/zhouxy/plusone/commons/exception/DbException.java
@@ -0,0 +1,19 @@
+package xyz.zhouxy.plusone.commons.exception;
+
+import com.google.common.annotations.Beta;
+
+@Beta
+public class DbException extends RuntimeException {
+
+ public DbException(String message) {
+ super(message);
+ }
+
+ public DbException(Throwable cause) {
+ super(cause);
+ }
+
+ public DbException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/xyz/zhouxy/plusone/commons/jdbc/JdbcUtil.java b/src/main/java/xyz/zhouxy/plusone/commons/jdbc/JdbcUtil.java
new file mode 100644
index 0000000..0eced64
--- /dev/null
+++ b/src/main/java/xyz/zhouxy/plusone/commons/jdbc/JdbcUtil.java
@@ -0,0 +1,145 @@
+package xyz.zhouxy.plusone.commons.jdbc;
+
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+
+import org.apache.commons.lang3.ArrayUtils;
+
+import com.google.common.annotations.Beta;
+
+import xyz.zhouxy.plusone.commons.util.DbRecord;
+import xyz.zhouxy.plusone.commons.util.MoreCollections;
+
+@Beta
+public class JdbcUtil {
+
+ public static JdbcExecutor connect(Connection conn) {
+ return new JdbcExecutor(conn);
+ }
+
+ private JdbcUtil() {
+ throw new IllegalStateException("Utility class");
+ }
+
+ public static class JdbcExecutor {
+
+ private final Connection conn;
+
+ public JdbcExecutor(Connection conn) {
+ this.conn = conn;
+ }
+
+ public List query(String sql, Object[] params, ResultMap resultMap) throws SQLException {
+ try (PreparedStatement stmt = this.conn.prepareStatement(sql)) {
+ if (params != null && params.length > 0) {
+ for (int i = 0; i < params.length; i++) {
+ stmt.setObject(i + 1, params[i]);
+ }
+ }
+ try (ResultSet rs = stmt.executeQuery()) {
+ List result = new ArrayList<>();
+ while (rs.next()) {
+ T e = resultMap.map(rs);
+ result.add(e);
+ }
+ return result;
+ }
+ }
+ }
+
+ public static final ResultMap