diff options
| author | Benjamin Culkin <scorpress@gmail.com> | 2025-12-07 17:14:16 -0500 |
|---|---|---|
| committer | Benjamin Culkin <scorpress@gmail.com> | 2025-12-07 17:17:24 -0500 |
| commit | e8e15d0335c7252f78b71c59905cb82c8964e80a (patch) | |
| tree | 8383609ce3166eeec2de78dbd149a91be547c59c | |
| parent | ce287c0a8e44a2bbbea32e475938779e22592519 (diff) | |
Add NamedPreparedStatement
Adds a version of PreparedStatement that supports named parameter
syntax.
3 files changed, 942 insertions, 1 deletions
diff --git a/base/src/examples/java/bjc/utils/examples/gui/panels/package-info.java b/base/src/examples/java/bjc/utils/examples/gui/panels/package-info.java new file mode 100644 index 0000000..8de90a2 --- /dev/null +++ b/base/src/examples/java/bjc/utils/examples/gui/panels/package-info.java @@ -0,0 +1 @@ +package bjc.utils.examples.gui.panels;
\ No newline at end of file diff --git a/base/src/main/java/bjc/utils/misc/NamedPreparedStatement.java b/base/src/main/java/bjc/utils/misc/NamedPreparedStatement.java new file mode 100644 index 0000000..b35029e --- /dev/null +++ b/base/src/main/java/bjc/utils/misc/NamedPreparedStatement.java @@ -0,0 +1,940 @@ +package bjc.utils.misc;
+
+import java.math.BigDecimal;
+import java.sql.*;
+import java.sql.Date;
+import java.util.*;
+
+/**
+ * NamedPreparedStatement
+ *
+ * Features
+ * - :name -> single parameter (bind once, all repeats covered)
+ * - @name -> list parameter (expands to ?,?,? based on values you set; repeats ok)
+ * - Empty-list toggle:
+ * .emptyListAsNull() // IN (@ids) -> IN (NULL) [default]
+ * .emptyListAsLiteral("YOUR_TEXT") // IN (@ids) -> IN (YOUR_TEXT)
+ * - Skips strings ('...'/\"...\") and comments (-- ..., /* ... * /)
+ * - Respects PostgreSQL type casts (::type)
+ * - Lazy compilation (or call compile() to do it eagerly)
+ *
+ * Usage:
+ * String sql = "SELECT * FROM t WHERE a = :a AND b IN (@bs)";
+ * try (NamedPreparedStatement nps = NamedPreparedStatement.prepare(conn, sql)) {
+ * nps.setInt("a", 7);
+ * nps.setList("bs", 1, 2, 3);
+ * // optional toggles
+ * nps.emptyListAsLiteral("SELECT NULL WHERE 1=0");
+ * // eager build if you need the PS now:
+ * PreparedStatement ps = nps.compile(); // built & bound, not executed
+ * try (ResultSet rs = ps.executeQuery()) { ... }
+ * }
+ */
+public final class NamedPreparedStatement implements AutoCloseable {
+
+ /* ---------- Construction ---------- */
+
+ private final Connection conn;
+ private final List<Token> tokens; // parsed tokens from original SQL
+ private final String originalSql;
+
+ private final Integer autoGeneratedKeys; // null if not requested
+ private final Integer resultSetType; // null unless custom rs type
+ private final Integer resultSetConcurrency; // null unless custom rs type
+
+ // current compiled statement
+ private PreparedStatement currentPs;
+ private String lastCompiledSql;
+
+ // parameter values
+ private final Map<String, Binder> singles = new HashMap<>();
+ private final Map<String, List<Binder>> lists = new HashMap<>();
+
+ // simple batch snapshots
+ private final List<ParamSnapshot> batch = new ArrayList<>();
+
+ // empty-list behavior
+ private EmptyListMode emptyListMode = EmptyListMode.AS_NULL;
+ private String emptyListLiteral = "NULL";
+
+ private NamedPreparedStatement(Connection conn, String sql, Integer autoGeneratedKeys, Integer resultSetType,
+ Integer resultSetConcurrency) {
+ this.conn = Objects.requireNonNull(conn, "conn");
+ this.originalSql = Objects.requireNonNull(sql, "sql");
+ this.tokens = parse(sql);
+ this.autoGeneratedKeys = autoGeneratedKeys;
+ this.resultSetType = resultSetType;
+ this.resultSetConcurrency = resultSetConcurrency;
+ }
+
+ /**
+ * Default options.
+ *
+ * @param conn The DB connection to use
+ * @param sql The SQL query to prepare
+ * @return The prepared statement, bound to the given DB connection
+ */
+ public static NamedPreparedStatement prepare(Connection conn, String sql) {
+ return new NamedPreparedStatement(conn, sql, null, null, null);
+ }
+
+ /**
+ * Request generated keys.
+ *
+ * @param conn The DB connection to use
+ * @param sql The SQL query to prepare
+ * @return The prepared statement, bound to the given DB connection and set to return generated keys.
+ */
+ public static NamedPreparedStatement prepareWithKeys(Connection conn, String sql) {
+ return new NamedPreparedStatement(conn, sql, Statement.RETURN_GENERATED_KEYS, null, null);
+ }
+
+ /**
+ * Custom result set type/concurrency.
+ *
+ * @param conn The DB connection to use
+ * @param sql The SQL query to prepare
+ * @param resultSetType The result-set type to use
+ * @param resultSetConcurrency The result-set concurrency to use
+ * @return The prepared statement, configured as specified
+ */
+ public static NamedPreparedStatement prepare(Connection conn, String sql, int resultSetType,
+ int resultSetConcurrency) {
+ return new NamedPreparedStatement(conn, sql, null, resultSetType, resultSetConcurrency);
+ }
+
+ /* ---------- Empty-list toggle ---------- */
+
+ /**
+ * Expand empty @lists as NULL (IN (@x) -> IN (NULL)).
+ *
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement emptyListAsNull() {
+ this.emptyListMode = EmptyListMode.AS_NULL;
+ return this;
+ }
+
+ /**
+ * Expand empty @lists as the provided literal (inserted verbatim). Example
+ * (Postgres): emptyListAsLiteral("SELECT NULL WHERE 1=0") → IN (@x) becomes IN
+ * (SELECT NULL WHERE 1=0)
+ *
+ * @param literal The literal to expand empty lists to
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement emptyListAsLiteral(String literal) {
+ if (literal == null || literal.isBlank())
+ throw new IllegalArgumentException("Empty-list literal must be non-blank.");
+ this.emptyListMode = EmptyListMode.AS_CUSTOM_LITERAL;
+ this.emptyListLiteral = literal;
+ return this;
+ }
+
+ /* ---------- Eager compile ---------- */
+
+ /**
+ * Build final SQL (expanding @lists), create PreparedStatement, and bind all
+ * parameters; does NOT execute. Returns the prepared statement instance.
+ *
+ * @return The fully bound prepared statement
+ * @throws SQLException If something went wrong preparing the statement
+ */
+ public PreparedStatement compile() throws SQLException {
+ buildAndBind();
+ return currentPs;
+ }
+
+ /* ---------- Execution ---------- */
+
+ /**
+ * Execute this statement
+ * @return The result of executing the query
+ * @throws SQLException If something went wrong executing the query
+ */
+ public ResultSet executeQuery() throws SQLException {
+ buildAndBind();
+ return currentPs.executeQuery();
+ }
+
+ /**
+ * Execute this update.
+ *
+ * @return The number of rows updated by the update
+ * @throws SQLException If something went wrong executing the update
+ */
+ public int executeUpdate() throws SQLException {
+ buildAndBind();
+ return currentPs.executeUpdate();
+ }
+
+ /**
+ * Execute the SQL statement
+ *
+ * @return The result of executing the SQL statement
+ * @throws SQLException If something went wrong executing the statement
+ */
+ public boolean execute() throws SQLException {
+ buildAndBind();
+ return currentPs.execute();
+ }
+
+ /**
+ * Execute one or more batch of parameters against this SQL statement
+ *
+ * @return The number of rows updated by each batch
+ * @throws SQLException If something went wrong executing the statement
+ */
+ public int[] executeBatch() throws SQLException {
+ if (batch.isEmpty()) {
+ buildAndBind();
+ return currentPs.executeBatch();
+ }
+ List<Integer> counts = new ArrayList<>(batch.size());
+ List<Binder> binders = buildPreparedStatement();
+
+ for (ParamSnapshot snap : batch) {
+ // restore snapshot
+ this.singles.clear();
+ this.lists.clear();
+ this.singles.putAll(snap.singles);
+ this.lists.putAll(snap.lists);
+
+ bindPreparedStatement(binders);
+
+ counts.add(currentPs.executeUpdate());
+ }
+ batch.clear();
+ return counts.stream().mapToInt(Integer::intValue).toArray();
+ }
+
+ /**
+ * Record the current set of parameters as a batch and start a new one.
+ *
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement addBatch() {
+ batch.add(new ParamSnapshot(new HashMap<>(singles), deepCopyLists(lists)));
+ return this;
+ }
+
+ /**
+ * Clear the currently bound parameters.
+ *
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement clearParameters() {
+ singles.clear();
+ lists.clear();
+ return this;
+ }
+
+ /**
+ * Get the generated keys for this prepared statement.
+ *
+ * See {@link PreparedStatement#getGeneratedKeys()}
+ *
+ * @return The generated keys for this prepared statement
+ * @throws SQLException If something went wrong getting the generated keys
+ */
+ public ResultSet getGeneratedKeys() throws SQLException {
+ if (currentPs == null)
+ throw new IllegalStateException("No statement compiled/executed yet.");
+ return currentPs.getGeneratedKeys();
+ }
+
+ /**
+ * Get the underlying prepared statement, or null if one hasn't been prepared yet
+ *
+ * TODO should this return a Optional?
+ *
+ * @return The underlying prepared statement, or null if there isn't one
+ */
+ public PreparedStatement getPreparedStatement() {
+ return currentPs;
+ }
+
+ /**
+ * Get the parsed SQL for this statement.
+ *
+ * @return The parsed SQL for this statement
+ */
+ public String getParsedSql() {
+ return lastCompiledSql;
+ }
+
+ /**
+ * Get the original SQL for this statement.
+ *
+ * @return The original SQL for this statement
+ */
+ public String getOriginalSql() {
+ return originalSql;
+ }
+
+ /**
+ * Get the available parameter names.
+ *
+ * @return The names of the available parameters
+ */
+ public Set<String> getParameterNames() {
+ LinkedHashSet<String> names = new LinkedHashSet<>();
+ for (Token t : tokens)
+ if (t instanceof ParamToken pt)
+ names.add(pt.name);
+ return Collections.unmodifiableSet(names);
+ }
+
+ @Override
+ public void close() throws SQLException {
+ if (currentPs != null)
+ currentPs.close();
+ }
+
+ @Override
+ public String toString() {
+ return "NamedPreparedStatement{sql='" + (lastCompiledSql != null ? lastCompiledSql : originalSql) + "'}";
+ }
+
+ /* ---------- Binding (single) ---------- */
+
+ /**
+ * Bind a object as a parameter
+ *
+ * @param name The name of the parameter
+ * @param value The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setObject(String name, Object value) {
+ singles.put(name, (ps, i) -> ps.setObject(i, value));
+ return this;
+ }
+
+ /**
+ * Bind a object as a parameter
+ *
+ * @param name The name of the parameter
+ * @param value The value for the parameter
+ * @param sqlType The SQL type for the parameter (see {@link java.sql.Types})
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setObject(String name, Object value, int sqlType) {
+ singles.put(name, (ps, i) -> {
+ if (value == null)
+ ps.setNull(i, sqlType);
+ else
+ ps.setObject(i, value, sqlType);
+ });
+ return this;
+ }
+
+ /**
+ * Bind a null as a parameter
+ *
+ * @param name The name of the parameter
+ * @param sqlType The SQL type for the parameter (see {@link java.sql.Types})
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setNull(String name, int sqlType) {
+ singles.put(name, (ps, i) -> ps.setNull(i, sqlType));
+ return this;
+ }
+
+ /**
+ * Bind a boolean as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setBoolean(String n, boolean x) {
+ singles.put(n, (ps, i) -> ps.setBoolean(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a byte as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setByte(String n, byte x) {
+ singles.put(n, (ps, i) -> ps.setByte(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a short as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setShort(String n, short x) {
+ singles.put(n, (ps, i) -> ps.setShort(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a int as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setInt(String n, int x) {
+ singles.put(n, (ps, i) -> ps.setInt(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a long as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setLong(String n, long x) {
+ singles.put(n, (ps, i) -> ps.setLong(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a float as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setFloat(String n, float x) {
+ singles.put(n, (ps, i) -> ps.setFloat(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a double as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setDouble(String n, double x) {
+ singles.put(n, (ps, i) -> ps.setDouble(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a {@link BigDecimal} as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setBigDecimal(String n, BigDecimal x) {
+ singles.put(n, (ps, i) -> ps.setBigDecimal(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a string as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setString(String n, String x) {
+ singles.put(n, (ps, i) -> ps.setString(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a byte-array as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setBytes(String n, byte[] x) {
+ singles.put(n, (ps, i) -> ps.setBytes(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a date as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setDate(String n, Date x) {
+ singles.put(n, (ps, i) -> ps.setDate(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a date as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @param c The calendar for the date
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setDate(String n, Date x, Calendar c) {
+ singles.put(n, (ps, i) -> ps.setDate(i, x, c));
+ return this;
+ }
+
+ /**
+ * Bind a time-value as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setTime(String n, Time x) {
+ singles.put(n, (ps, i) -> ps.setTime(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a time-value as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @param c The calendar for the time
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setTime(String n, Time x, Calendar c) {
+ singles.put(n, (ps, i) -> ps.setTime(i, x, c));
+ return this;
+ }
+
+ /**
+ * Bind a timestamp as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setTimestamp(String n, Timestamp x) {
+ singles.put(n, (ps, i) -> ps.setTimestamp(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a timestamp as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @param c The calendar for the timestamp
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setTimestamp(String n, Timestamp x, Calendar c) {
+ singles.put(n, (ps, i) -> ps.setTimestamp(i, x, c));
+ return this;
+ }
+
+ /**
+ * Bind a {@link Array} as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setArray(String n, Array x) {
+ singles.put(n, (ps, i) -> ps.setArray(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a {@link Blob} as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setBlob(String n, Blob x) {
+ singles.put(n, (ps, i) -> ps.setBlob(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a {@link Clob} as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setClob(String n, Clob x) {
+ singles.put(n, (ps, i) -> ps.setClob(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a URL as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setURL(String n, java.net.URL x) {
+ singles.put(n, (ps, i) -> ps.setURL(i, x));
+ return this;
+ }
+
+ /**
+ * Bind a ASCII stream as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @param len The length of the stream
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setAsciiStream(String n, java.io.InputStream x, int len) {
+ singles.put(n, (ps, i) -> ps.setAsciiStream(i, x, len));
+ return this;
+ }
+
+ /**
+ * Bind a binary stream as a parameter
+ *
+ * @param n The name of the parameter
+ * @param x The value for the parameter
+ * @param len The length of the stream
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setBinaryStream(String n, java.io.InputStream x, int len) {
+ singles.put(n, (ps, i) -> ps.setBinaryStream(i, x, len));
+ return this;
+ }
+
+ /**
+ * Bind a character stream as a parameter
+ *
+ * @param n The name of the parameter
+ * @param r The value for the parameter
+ * @param len The length of the stream
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setCharacterStream(String n, java.io.Reader r, int len) {
+ singles.put(n, (ps, i) -> ps.setCharacterStream(i, r, len));
+ return this;
+ }
+
+ /* ---------- Binding (lists) ---------- */
+
+ /**
+ * Bind a list parameter to the statement
+ *
+ * @param name The name of the list parameter
+ * @param values The values for the list parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setList(String name, Collection<?> values) {
+ lists.put(name, toBinders(values, Types.OTHER, false));
+ return this;
+ }
+
+ /**
+ * Bind a list parameter to the statement
+ *
+ * @param name The name of the list parameter
+ * @param values The values for the list parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setList(String name, Object... values) {
+ lists.put(name, toBinders(Arrays.asList(values), Types.OTHER, false));
+ return this;
+ }
+
+ /**
+ * Explicit SQL type for all elements (useful if nulls might appear).
+ *
+ * @param name The name of the list parameter
+ * @param sqlType The SQL type for the parameters
+ * @param values The values for the list parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setList(String name, int sqlType, Collection<?> values) {
+ lists.put(name, toBinders(values, sqlType, true));
+ return this;
+ }
+
+ /**
+ * Explicit SQL type for all elements (useful if nulls might appear).
+ *
+ * @param name The name of the list parameter
+ * @param sqlType The SQL type for the parameters
+ * @param values The values for the list parameter
+ * @return The prepared statement
+ */
+ public NamedPreparedStatement setList(String name, int sqlType, Object... values) {
+ lists.put(name, toBinders(Arrays.asList(values), sqlType, true));
+ return this;
+ }
+
+ /* ---------- Build & bind ---------- */
+
+ private void buildAndBind() throws SQLException {
+ List<Binder> bindersInOrder = buildPreparedStatement();
+
+ bindPreparedStatement(bindersInOrder);
+ }
+
+ private void bindPreparedStatement(List<Binder> bindersInOrder) throws SQLException {
+ int idx = 1;
+ for (Binder b : bindersInOrder)
+ b.bind(currentPs, idx++);
+ }
+
+ private List<Binder> buildPreparedStatement() throws SQLException {
+ StringBuilder sb = new StringBuilder(originalSql.length() + 32);
+ List<Binder> bindersInOrder = new ArrayList<>(32);
+
+ for (Token t : tokens) {
+ if (t instanceof TextToken tt) {
+ sb.append(tt.text);
+ } else {
+ ParamToken pt = (ParamToken) t;
+ if (!pt.list) { // :name
+ Binder b = singles.get(pt.name);
+ if (b == null)
+ throw new IllegalArgumentException("Missing value for parameter :" + pt.name);
+ sb.append('?');
+ bindersInOrder.add(b);
+ } else { // @name
+ List<Binder> lst = lists.get(pt.name);
+ if (lst == null)
+ throw new IllegalArgumentException("Missing list for parameter @" + pt.name);
+ if (lst.isEmpty()) {
+ if (emptyListMode == EmptyListMode.AS_NULL) {
+ sb.append("NULL");
+ } else {
+ sb.append(emptyListLiteral);
+ }
+ } else {
+ for (int i = 0; i < lst.size(); i++) {
+ if (i > 0)
+ sb.append(',');
+ sb.append('?');
+ bindersInOrder.add(lst.get(i));
+ }
+ }
+ }
+ }
+ }
+
+ this.lastCompiledSql = sb.toString();
+
+ if (currentPs != null)
+ try {
+ currentPs.close();
+ } catch (SQLException ignore) {
+ }
+
+ if (autoGeneratedKeys != null) {
+ currentPs = conn.prepareStatement(lastCompiledSql, autoGeneratedKeys);
+ } else if (resultSetType != null && resultSetConcurrency != null) {
+ currentPs = conn.prepareStatement(lastCompiledSql, resultSetType, resultSetConcurrency);
+ } else {
+ currentPs = conn.prepareStatement(lastCompiledSql);
+ }
+ return bindersInOrder;
+ }
+
+ /* ---------- Parser ---------- */
+
+ private interface Token {
+ }
+
+ private static final class TextToken implements Token {
+ final String text;
+
+ TextToken(String s) {
+ this.text = s;
+ }
+ }
+
+ private static final class ParamToken implements Token {
+ final String name;
+ final boolean list;
+
+ ParamToken(String n, boolean l) {
+ this.name = n;
+ this.list = l;
+ }
+ }
+
+ private static List<Token> parse(String sql) {
+ ArrayList<Token> out = new ArrayList<>();
+ StringBuilder buf = new StringBuilder();
+
+ boolean inSingle = false, inDouble = false, inLine = false, inBlock = false;
+ int n = sql.length();
+
+ for (int i = 0; i < n; i++) {
+ char c = sql.charAt(i);
+
+ if (inLine) {
+ buf.append(c);
+ if (c == '\n' || c == '\r')
+ inLine = false;
+ continue;
+ }
+ if (inBlock) {
+ buf.append(c);
+ if (c == '*' && i + 1 < n && sql.charAt(i + 1) == '/') {
+ buf.append('/');
+ i++;
+ inBlock = false;
+ }
+ continue;
+ }
+ if (inSingle) {
+ buf.append(c);
+ if (c == '\'') {
+ if (i + 1 < n && sql.charAt(i + 1) == '\'') {
+ buf.append('\'');
+ i++;
+ } else
+ inSingle = false;
+ }
+ continue;
+ }
+ if (inDouble) {
+ buf.append(c);
+ if (c == '"') {
+ if (i + 1 < n && sql.charAt(i + 1) == '"') {
+ buf.append('"');
+ i++;
+ } else
+ inDouble = false;
+ }
+ continue;
+ }
+
+ if (c == '-' && i + 1 < n && sql.charAt(i + 1) == '-') {
+ buf.append("--");
+ i++;
+ inLine = true;
+ continue;
+ }
+ if (c == '/' && i + 1 < n && sql.charAt(i + 1) == '*') {
+ buf.append("/*");
+ i++;
+ inBlock = true;
+ continue;
+ }
+ if (c == '\'') {
+ buf.append(c);
+ inSingle = true;
+ continue;
+ }
+ if (c == '"') {
+ buf.append(c);
+ inDouble = true;
+ continue;
+ }
+
+ if (c == ':' || c == '@') {
+ char prev = (i > 0) ? sql.charAt(i - 1) : '\0';
+ if ((c == ':' && prev == ':') || (c == '@' && prev == '@')) {
+ buf.append(c);
+ continue;
+ } // ::type or @@sys
+ if (i + 1 < n && isIdentStart(sql.charAt(i + 1))) {
+ if (buf.length() > 0) {
+ out.add(new TextToken(buf.toString()));
+ buf.setLength(0);
+ }
+ int j = i + 2;
+ while (j < n && isIdentPart(sql.charAt(j)))
+ j++;
+ String name = sql.substring(i + 1, j);
+ out.add(new ParamToken(name, c == '@'));
+ i = j - 1;
+ continue;
+ }
+ }
+
+ buf.append(c);
+ }
+ if (buf.length() > 0)
+ out.add(new TextToken(buf.toString()));
+ return out;
+ }
+
+ private static boolean isIdentStart(char c) {
+ return c == '_' || Character.isLetter(c);
+ }
+
+ private static boolean isIdentPart(char c) {
+ return c == '_' || Character.isLetterOrDigit(c);
+ }
+
+ /* ---------- Helpers ---------- */
+
+ @FunctionalInterface
+ private interface Binder {
+ void bind(PreparedStatement ps, int index) throws SQLException;
+ }
+
+ private static List<Binder> toBinders(Collection<?> values, int sqlType, boolean forceType) {
+ ArrayList<Binder> list = new ArrayList<>(values.size());
+ for (Object v : values) {
+ final Object val = v;
+ if (forceType) {
+ list.add((ps, i) -> {
+ if (val == null)
+ ps.setNull(i, sqlType);
+ else
+ ps.setObject(i, val, sqlType);
+ });
+ } else {
+ list.add((ps, i) -> ps.setObject(i, val));
+ }
+ }
+ return list;
+ }
+
+ private static Map<String, List<Binder>> deepCopyLists(Map<String, List<Binder>> src) {
+ Map<String, List<Binder>> m = new HashMap<>(src.size());
+ for (Map.Entry<String, List<Binder>> e : src.entrySet()) {
+ m.put(e.getKey(), new ArrayList<>(e.getValue()));
+ }
+ return m;
+ }
+
+ private static class ParamSnapshot {
+ public Map<String, Binder> singles;
+ public Map<String, List<Binder>> lists;
+
+ public ParamSnapshot(Map<String, Binder> singles, Map<String, List<Binder>> lists) {
+ super();
+ this.singles = singles;
+ this.lists = lists;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(lists, singles);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ ParamSnapshot other = (ParamSnapshot) obj;
+ return Objects.equals(lists, other.lists) && Objects.equals(singles, other.singles);
+ }
+ }
+
+ private enum EmptyListMode {
+ AS_NULL, AS_CUSTOM_LITERAL
+ }
+}
diff --git a/base/src/main/java/module-info.java b/base/src/main/java/module-info.java index 525eb53..9d91e01 100644 --- a/base/src/main/java/module-info.java +++ b/base/src/main/java/module-info.java @@ -37,5 +37,5 @@ module bjc.utils { requires transitive java.desktop; requires java.logging; requires junit; - requires java.sql; + requires transitive java.sql; }
\ No newline at end of file |
