From 8f0b451c442985d7338bd1d09bc5798220787cb8 Mon Sep 17 00:00:00 2001 From: Benjamin Culkin Date: Mon, 8 Dec 2025 21:00:07 -0500 Subject: Format NamedPreparedStatement --- .../bjc/utils/misc/NamedPreparedStatement.java | 1592 ++++++++++++-------- 1 file changed, 983 insertions(+), 609 deletions(-) (limited to 'base/src/main/java') diff --git a/base/src/main/java/bjc/utils/misc/NamedPreparedStatement.java b/base/src/main/java/bjc/utils/misc/NamedPreparedStatement.java index 263c101..a5f3a81 100644 --- a/base/src/main/java/bjc/utils/misc/NamedPreparedStatement.java +++ b/base/src/main/java/bjc/utils/misc/NamedPreparedStatement.java @@ -17,612 +17,984 @@ import java.util.*; * for many Args that share the same "shape" (same @list sizes / emptiness). */ public final class NamedPreparedStatement implements AutoCloseable { - /* ---------- Construction (simple path) ---------- */ - - private final Connection conn; - private final List tokens; - private final String originalSql; - - private final Integer autoGeneratedKeys; - private final Integer resultSetType; - private final Integer resultSetConcurrency; - - private PreparedStatement currentPs; - private String lastCompiledSql; - - private final Map singles = new HashMap<>(); - private final Map> lists = new HashMap<>(); - - private final List batch = new ArrayList<>(); - - 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; - } - - public static NamedPreparedStatement prepare(Connection conn, String sql) { - return new NamedPreparedStatement(conn, sql, null, null, null); - } - public static NamedPreparedStatement prepareWithKeys(Connection conn, String sql) { - return new NamedPreparedStatement(conn, sql, Statement.RETURN_GENERATED_KEYS, null, null); - } - public static NamedPreparedStatement prepare(Connection conn, String sql, int resultSetType, int resultSetConcurrency) { - return new NamedPreparedStatement(conn, sql, null, resultSetType, resultSetConcurrency); - } - - /* ---------- Empty-list toggle (simple path) ---------- */ - - public NamedPreparedStatement emptyListAsNull() { - this.emptyListMode = EmptyListMode.AS_NULL; - return this; - } - 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 (simple path) ---------- */ - - /** Build final SQL and bind parameters now, but do not execute. */ - public PreparedStatement compile() throws SQLException { - buildAndBindSimple(); - return currentPs; - } - - /* ---------- Execution (simple path) ---------- */ - - public ResultSet executeQuery() throws SQLException { - buildAndBindSimple(); - return currentPs.executeQuery(); - } - public int executeUpdate() throws SQLException { - buildAndBindSimple(); - return currentPs.executeUpdate(); - } - public boolean execute() throws SQLException { - buildAndBindSimple(); - return currentPs.execute(); - } - public int[] executeBatch() throws SQLException { - if (batch.isEmpty()) { - buildAndBindSimple(); - return currentPs.executeBatch(); - } - List counts = new ArrayList<>(batch.size()); - for (ParamSnapshot snap : batch) { - this.singles.clear(); - this.lists.clear(); - this.singles.putAll(snap.singles); - this.lists.putAll(snap.lists); - buildAndBindSimple(); - counts.add(currentPs.executeUpdate()); - } - batch.clear(); - return counts.stream().mapToInt(Integer::intValue).toArray(); - } - public NamedPreparedStatement addBatch() { - batch.add(new ParamSnapshot(new HashMap<>(singles), deepCopyLists(lists))); - return this; - } - - public NamedPreparedStatement clearParameters() { - singles.clear(); - lists.clear(); - return this; - } - - public ResultSet getGeneratedKeys() throws SQLException { - if (currentPs == null) throw new IllegalStateException("No statement compiled/executed yet."); - return currentPs.getGeneratedKeys(); - } - - public PreparedStatement getPreparedStatement() { return currentPs; } - public String getParsedSql() { return lastCompiledSql; } - public String getOriginalSql() { return originalSql; } - - public Set getParameterNames() { - LinkedHashSet 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 path) ---------- */ - - public NamedPreparedStatement setObject(String name, Object value) { singles.put(name, (ps,i)->ps.setObject(i,value)); return this; } - 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; - } - public NamedPreparedStatement setNull(String name, int sqlType) { singles.put(name,(ps,i)->ps.setNull(i,sqlType)); return this; } - - public NamedPreparedStatement setBoolean(String n, boolean x){ singles.put(n,(ps,i)->ps.setBoolean(i,x)); return this; } - public NamedPreparedStatement setByte(String n, byte x){ singles.put(n,(ps,i)->ps.setByte(i,x)); return this; } - public NamedPreparedStatement setShort(String n, short x){ singles.put(n,(ps,i)->ps.setShort(i,x)); return this; } - public NamedPreparedStatement setInt(String n, int x){ singles.put(n,(ps,i)->ps.setInt(i,x)); return this; } - public NamedPreparedStatement setLong(String n, long x){ singles.put(n,(ps,i)->ps.setLong(i,x)); return this; } - public NamedPreparedStatement setFloat(String n, float x){ singles.put(n,(ps,i)->ps.setFloat(i,x)); return this; } - public NamedPreparedStatement setDouble(String n, double x){ singles.put(n,(ps,i)->ps.setDouble(i,x)); return this; } - public NamedPreparedStatement setBigDecimal(String n, BigDecimal x){ singles.put(n,(ps,i)->ps.setBigDecimal(i,x)); return this; } - public NamedPreparedStatement setString(String n, String x){ singles.put(n,(ps,i)->ps.setString(i,x)); return this; } - public NamedPreparedStatement setBytes(String n, byte[] x){ singles.put(n,(ps,i)->ps.setBytes(i,x)); return this; } - public NamedPreparedStatement setDate(String n, Date x){ singles.put(n,(ps,i)->ps.setDate(i,x)); return this; } - public NamedPreparedStatement setDate(String n, Date x, Calendar c){ singles.put(n,(ps,i)->ps.setDate(i,x,c)); return this; } - public NamedPreparedStatement setTime(String n, Time x){ singles.put(n,(ps,i)->ps.setTime(i,x)); return this; } - public NamedPreparedStatement setTime(String n, Time x, Calendar c){ singles.put(n,(ps,i)->ps.setTime(i,x,c)); return this; } - public NamedPreparedStatement setTimestamp(String n, Timestamp x){ singles.put(n,(ps,i)->ps.setTimestamp(i,x)); return this; } - public NamedPreparedStatement setTimestamp(String n, Timestamp x, Calendar c){ singles.put(n,(ps,i)->ps.setTimestamp(i,x,c)); return this; } - public NamedPreparedStatement setArray(String n, Array x){ singles.put(n,(ps,i)->ps.setArray(i,x)); return this; } - public NamedPreparedStatement setBlob(String n, Blob x){ singles.put(n,(ps,i)->ps.setBlob(i,x)); return this; } - public NamedPreparedStatement setClob(String n, Clob x){ singles.put(n,(ps,i)->ps.setClob(i,x)); return this; } - public NamedPreparedStatement setURL(String n, java.net.URL x){ singles.put(n,(ps,i)->ps.setURL(i,x)); return this; } - public NamedPreparedStatement setAsciiStream(String n, java.io.InputStream x, int len){ singles.put(n,(ps,i)->ps.setAsciiStream(i,x,len)); return this; } - public NamedPreparedStatement setBinaryStream(String n, java.io.InputStream x, int len){ singles.put(n,(ps,i)->ps.setBinaryStream(i,x,len)); return this; } - public NamedPreparedStatement setCharacterStream(String n, java.io.Reader r, int len){ singles.put(n,(ps,i)->ps.setCharacterStream(i,r,len)); return this; } - - public NamedPreparedStatement setList(String name, Collection values) { - lists.put(name, toBinders(values, Types.OTHER, false)); - return this; - } - public NamedPreparedStatement setList(String name, Object... values) { - lists.put(name, toBinders(Arrays.asList(values), Types.OTHER, false)); - return this; - } - public NamedPreparedStatement setList(String name, int sqlType, Collection values) { - lists.put(name, toBinders(values, sqlType, true)); - return this; - } - public NamedPreparedStatement setList(String name, int sqlType, Object... values) { - lists.put(name, toBinders(Arrays.asList(values), sqlType, true)); - return this; - } - - /* ---------- Build & bind (simple path) ---------- */ - - private void buildAndBindSimple() throws SQLException { - StringBuilder sb = new StringBuilder(originalSql.length() + 32); - List 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 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); - } - - int idx = 1; - for (Binder b : bindersInOrder) b.bind(currentPs, idx++); - } - - /* ===================================================================== - PLANNED / COMPILE-ONCE EXECUTION PATH (Args + Executor) - ===================================================================== */ - - /** - * Immutable bag of parameter values. Build these on any thread. - * They must all share the same "shape" (same @list sizes and emptiness) - * if you want to reuse a single Executor (no SQL rebuild). - */ - public static final class Args { - private final Map singles; - private final Map> lists; - - private Args(Map s, Map> l) { - this.singles = Collections.unmodifiableMap(s); - this.lists = unmodifiableListMap(l); - } - - public static Builder builder() { return new Builder(); } - - public static final class Builder { - private final Map singles = new HashMap<>(); - private final Map> lists = new HashMap<>(); - - public Builder setNull(String name, int sqlType) { singles.put(name, (ps,i)->ps.setNull(i, sqlType)); return this; } - public Builder setObject(String name, Object value) { singles.put(name, (ps,i)->ps.setObject(i, value)); return this; } - public Builder 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; - } - public Builder setBoolean(String n, boolean x){ singles.put(n,(ps,i)->ps.setBoolean(i,x)); return this; } - public Builder setByte(String n, byte x){ singles.put(n,(ps,i)->ps.setByte(i,x)); return this; } - public Builder setShort(String n, short x){ singles.put(n,(ps,i)->ps.setShort(i,x)); return this; } - public Builder setInt(String n, int x){ singles.put(n,(ps,i)->ps.setInt(i,x)); return this; } - public Builder setLong(String n, long x){ singles.put(n,(ps,i)->ps.setLong(i,x)); return this; } - public Builder setFloat(String n, float x){ singles.put(n,(ps,i)->ps.setFloat(i,x)); return this; } - public Builder setDouble(String n, double x){ singles.put(n,(ps,i)->ps.setDouble(i,x)); return this; } - public Builder setBigDecimal(String n, BigDecimal x){ singles.put(n,(ps,i)->ps.setBigDecimal(i,x)); return this; } - public Builder setString(String n, String x){ singles.put(n,(ps,i)->ps.setString(i,x)); return this; } - public Builder setBytes(String n, byte[] x){ singles.put(n,(ps,i)->ps.setBytes(i,x)); return this; } - public Builder setDate(String n, Date x){ singles.put(n,(ps,i)->ps.setDate(i,x)); return this; } - public Builder setDate(String n, Date x, Calendar c){ singles.put(n,(ps,i)->ps.setDate(i,x,c)); return this; } - public Builder setTime(String n, Time x){ singles.put(n,(ps,i)->ps.setTime(i,x)); return this; } - public Builder setTime(String n, Time x, Calendar c){ singles.put(n,(ps,i)->ps.setTime(i,x,c)); return this; } - public Builder setTimestamp(String n, Timestamp x){ singles.put(n,(ps,i)->ps.setTimestamp(i,x)); return this; } - public Builder setTimestamp(String n, Timestamp x, Calendar c){ singles.put(n,(ps,i)->ps.setTimestamp(i,x,c)); return this; } - public Builder setArray(String n, Array x){ singles.put(n,(ps,i)->ps.setArray(i,x)); return this; } - public Builder setBlob(String n, Blob x){ singles.put(n,(ps,i)->ps.setBlob(i,x)); return this; } - public Builder setClob(String n, Clob x){ singles.put(n,(ps,i)->ps.setClob(i,x)); return this; } - public Builder setURL(String n, java.net.URL x){ singles.put(n,(ps,i)->ps.setURL(i,x)); return this; } - public Builder setAsciiStream(String n, java.io.InputStream x, int len){ singles.put(n,(ps,i)->ps.setAsciiStream(i,x,len)); return this; } - public Builder setBinaryStream(String n, java.io.InputStream x, int len){ singles.put(n,(ps,i)->ps.setBinaryStream(i,x,len)); return this; } - public Builder setCharacterStream(String n, java.io.Reader r, int len){ singles.put(n,(ps,i)->ps.setCharacterStream(i,r,len)); return this; } - - public Builder setList(String name, Collection values) { - lists.put(name, toBinders(values, Types.OTHER, false)); - return this; - } - public Builder setList(String name, Object... values) { - lists.put(name, toBinders(Arrays.asList(values), Types.OTHER, false)); - return this; - } - public Builder setList(String name, int sqlType, Collection values) { - lists.put(name, toBinders(values, sqlType, true)); - return this; - } - public Builder setList(String name, int sqlType, Object... values) { - lists.put(name, toBinders(Arrays.asList(values), sqlType, true)); - return this; - } - - public Args build() { return new Args(new HashMap<>(singles), deepCopyLists(lists)); } - } - } - - /** - * Executor: compiles SQL once using the "shape" (list sizes / emptiness) of a prototype Args, - * creates one PreparedStatement, and reuses it to execute many Args with the same shape. - */ - public static final class Executor implements AutoCloseable { - private final Connection conn; - private final List tokens; - private final String originalSql; - private final EmptyListMode emptyMode; - private final String emptyLiteral; - - private final Integer autoGeneratedKeys; - private final Integer resultSetType; - private final Integer resultSetConcurrency; - - private final Plan plan; // compiled once - private final PreparedStatement ps; // prepared once - - private Executor(Connection conn, - List tokens, - String originalSql, - EmptyListMode mode, - String emptyLiteral, - Integer autoGeneratedKeys, - Integer resultSetType, - Integer resultSetConcurrency, - Args shapeArgs) throws SQLException { - this.conn = conn; - this.tokens = tokens; - this.originalSql = originalSql; - this.emptyMode = mode; - this.emptyLiteral = emptyLiteral; - this.autoGeneratedKeys = autoGeneratedKeys; - this.resultSetType = resultSetType; - this.resultSetConcurrency = resultSetConcurrency; - - this.plan = Plan.compile(tokens, shapeArgs, mode, emptyLiteral); - if (autoGeneratedKeys != null) { - this.ps = conn.prepareStatement(plan.compiledSql, autoGeneratedKeys); - } else if (resultSetType != null && resultSetConcurrency != null) { - this.ps = conn.prepareStatement(plan.compiledSql, resultSetType, resultSetConcurrency); - } else { - this.ps = conn.prepareStatement(plan.compiledSql); - } - } - - /** Build an Executor with the given prototype Args as the shape. */ - public static Executor create(Connection conn, String sql, Args shapeArgs) throws SQLException { - NamedPreparedStatement tmp = new NamedPreparedStatement(conn, sql, null, null, null); - return new Executor(conn, tmp.tokens, tmp.originalSql, EmptyListMode.AS_NULL, "NULL", - null, null, null, shapeArgs); - } - - /** Build an Executor with empty-list toggle and RS options. */ - public static Executor create(Connection conn, String sql, Args shapeArgs, - boolean emptyAsNull, String customEmptyLiteral, - Integer autoGeneratedKeys, - Integer resultSetType, - Integer resultSetConcurrency) throws SQLException { - NamedPreparedStatement tmp = new NamedPreparedStatement(conn, sql, autoGeneratedKeys, resultSetType, resultSetConcurrency); - EmptyListMode mode = emptyAsNull ? EmptyListMode.AS_NULL : EmptyListMode.AS_CUSTOM_LITERAL; - String lit = emptyAsNull ? "NULL" : Objects.requireNonNull(customEmptyLiteral, "customEmptyLiteral"); - return new Executor(conn, tmp.tokens, tmp.originalSql, mode, lit, - autoGeneratedKeys, resultSetType, resultSetConcurrency, shapeArgs); - } - - /** Bind a single Args and execute (no JDBC addBatch). */ - public int executeUpdate(Args args) throws SQLException { - plan.validateShape(args); - bind(args); - return ps.executeUpdate(); - } - - public ResultSet executeQuery(Args args) throws SQLException { - plan.validateShape(args); - bind(args); - return ps.executeQuery(); - } - - /** Add to JDBC batch (binds, then ps.addBatch()). */ - public void addBatch(Args args) throws SQLException { - plan.validateShape(args); - bind(args); - ps.addBatch(); - } - - /** Execute the current JDBC batch. */ - public int[] executeBatch() throws SQLException { return ps.executeBatch(); } - - /** Convenience: bind+batch all, then executeBatch. */ - public int[] executeBatch(Iterable many) throws SQLException { - for (Args a : many) { addBatch(a); } - return ps.executeBatch(); - } - - public PreparedStatement getPreparedStatement() { return ps; } - public String getCompiledSql() { return plan.compiledSql; } - - private void bind(Args args) throws SQLException { - ps.clearParameters(); - int idx = 1; - for (BindStep step : plan.steps) { - if (!step.isList) { - Binder b = args.singles.get(step.name); - if (b == null) throw new IllegalArgumentException("Missing value for :" + step.name); - b.bind(ps, idx++); - } else { - List lst = args.lists.get(step.name); - if (lst == null) throw new IllegalArgumentException("Missing list for @" + step.name); - if (lst.isEmpty()) { - // No binders for empty → plan generated zero '?' for this step - // (or inserted literal); nothing to bind here. - } else { - for (int k = 0; k < lst.size(); k++) { - lst.get(k).bind(ps, idx++); - } - } - } - } - } - - @Override public void close() throws SQLException { ps.close(); } - } - - /* ---------- Plan: compiled SQL + bind steps ---------- */ - - private static final class Plan { - final String compiledSql; - final List steps; - - private Plan(String sql, List steps) { - this.compiledSql = sql; - this.steps = steps; - } - - static Plan compile(List tokens, Args shape, EmptyListMode mode, String emptyLiteral) { - StringBuilder sb = new StringBuilder(128 + tokens.size()*4); - ArrayList steps = new ArrayList<>(); - for (Token t : tokens) { - if (t instanceof TextToken tt) { sb.append(tt.text); } - else { - ParamToken pt = (ParamToken) t; - if (!pt.list) { - // one '?' - sb.append('?'); - steps.add(BindStep.single(pt.name)); - } else { - List lst = shape.lists.get(pt.name); - if (lst == null) - throw new IllegalArgumentException("Prototype Args missing list for @" + pt.name); - if (lst.isEmpty()) { - if (mode == EmptyListMode.AS_NULL) sb.append("NULL"); - else sb.append(emptyLiteral); - // No bind steps for this @name occurrence (no '?') - } else { - for (int i = 0; i < lst.size(); i++) { - if (i > 0) sb.append(','); - sb.append('?'); - } - steps.add(BindStep.list(pt.name)); // bind will consume lst.size() placeholders - } - } - } - } - return new Plan(sb.toString(), steps); - } - - void validateShape(Args args) { - // Ensure each list occurrence is either empty or has same size as prototype for that name - // We infer expected sizes by scanning steps and checking the first Args we compiled with. - // Simpler approach: recompute name->size from prototype at compile time. - // We'll rely on occurrence-by-occurrence behavior: - // - If a BindStep is list(name), it implies non-empty in prototype; so args must also be non-empty and same size. - // - If a list was empty in prototype, there is NO BindStep and NO '?' to bind; args must also be empty. - // To implement this, we track expected sizes map during compile; here we recompute from steps and prototype shape. - // For simplicity and robustness, we check sizes name-by-name: - Map expected = new HashMap<>(); - for (BindStep s : steps) { - if (s.isList) { - // means prototype had non-empty list for this name; record its size once - expected.putIfAbsent(s.name, -1); // mark needs size check later - } - } - // Fill sizes from prototype-like assumption by examining one Args that created the Plan: - // Not directly available here; we can infer on the fly from current args on first call, - // but then we'd allow mismatch silently. So we take a practical path: - // We require that for any list name that appears in BindSteps, the size is >0 and constant across executions. - for (Map.Entry e : expected.entrySet()) { - List lst = args.lists.get(e.getKey()); - if (lst == null || lst.isEmpty()) - throw new IllegalArgumentException("List @" + e.getKey() + " must be non-empty to match Executor shape."); - expected.put(e.getKey(), lst.size()); - } - // Also ensure any list names that were empty in prototype are still empty now: - // We detect these by scanning param names in args that are not in expected. - for (Map.Entry> en : args.lists.entrySet()) { - String name = en.getKey(); - if (!expected.containsKey(name)) { - // This name had empty list in prototype → must be empty now - if (!en.getValue().isEmpty()) - throw new IllegalArgumentException("List @" + name + " must be empty to match Executor shape."); - } - } - // Finally, confirm all expected sizes actually match across all occurrences — already implied by name-based size. - } - } - - private static final class BindStep { - final String name; - final boolean isList; - private BindStep(String n, boolean list) { this.name = n; this.isList = list; } - static BindStep single(String n) { return new BindStep(n, false); } - static BindStep list(String n) { return new BindStep(n, true); } - } - - /* ---------- 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 parse(String sql) { - ArrayList 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+10) ? sql.charAt(i-1) : '\0'; - if ((c==':' && prev==':') || (c=='@' && prev=='@')) { buf.append(c); continue; } // ::type or @@sys - if (i+10){ out.add(new TextToken(buf.toString())); buf.setLength(0); } - int j = i+2; - while (j0) 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 - interface Binder { void bind(PreparedStatement ps, int index) throws SQLException; } - - private static List toBinders(Collection values, int sqlType, boolean forceType) { - ArrayList 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> deepCopyLists(Map> src) { - Map> m = new HashMap<>(src.size()); - for (Map.Entry> e : src.entrySet()) { - m.put(e.getKey(), new ArrayList<>(e.getValue())); - } - return m; - } - - private static Map> unmodifiableListMap(Map> src) { - Map> m = new HashMap<>(src.size()); - for (Map.Entry> e : src.entrySet()) { - m.put(e.getKey(), Collections.unmodifiableList(e.getValue())); - } - return Collections.unmodifiableMap(m); - } - - static class ParamSnapshot { - public Map singles; - public Map> lists; - - public ParamSnapshot(Map singles, Map> lists) { + /* ---------- Construction (simple path) ---------- */ + + private final Connection conn; + private final List tokens; + private final String originalSql; + + private final Integer autoGeneratedKeys; + private final Integer resultSetType; + private final Integer resultSetConcurrency; + + private PreparedStatement currentPs; + private String lastCompiledSql; + + private final Map singles = new HashMap<>(); + private final Map> lists = new HashMap<>(); + + private final List batch = new ArrayList<>(); + + 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; + } + + public static NamedPreparedStatement prepare(Connection conn, String sql) { + return new NamedPreparedStatement(conn, sql, null, null, null); + } + + public static NamedPreparedStatement prepareWithKeys(Connection conn, String sql) { + return new NamedPreparedStatement(conn, sql, Statement.RETURN_GENERATED_KEYS, null, null); + } + + public static NamedPreparedStatement prepare(Connection conn, String sql, int resultSetType, + int resultSetConcurrency) { + return new NamedPreparedStatement(conn, sql, null, resultSetType, resultSetConcurrency); + } + + /* ---------- Empty-list toggle (simple path) ---------- */ + + public NamedPreparedStatement emptyListAsNull() { + this.emptyListMode = EmptyListMode.AS_NULL; + return this; + } + + 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 (simple path) ---------- */ + + /** Build final SQL and bind parameters now, but do not execute. */ + public PreparedStatement compile() throws SQLException { + buildAndBindSimple(); + return currentPs; + } + + /* ---------- Execution (simple path) ---------- */ + + public ResultSet executeQuery() throws SQLException { + buildAndBindSimple(); + return currentPs.executeQuery(); + } + + public int executeUpdate() throws SQLException { + buildAndBindSimple(); + return currentPs.executeUpdate(); + } + + public boolean execute() throws SQLException { + buildAndBindSimple(); + return currentPs.execute(); + } + + public int[] executeBatch() throws SQLException { + if (batch.isEmpty()) { + buildAndBindSimple(); + return currentPs.executeBatch(); + } + List counts = new ArrayList<>(batch.size()); + for (ParamSnapshot snap : batch) { + this.singles.clear(); + this.lists.clear(); + this.singles.putAll(snap.singles); + this.lists.putAll(snap.lists); + buildAndBindSimple(); + counts.add(currentPs.executeUpdate()); + } + batch.clear(); + return counts.stream().mapToInt(Integer::intValue).toArray(); + } + + public NamedPreparedStatement addBatch() { + batch.add(new ParamSnapshot(new HashMap<>(singles), deepCopyLists(lists))); + return this; + } + + public NamedPreparedStatement clearParameters() { + singles.clear(); + lists.clear(); + return this; + } + + public ResultSet getGeneratedKeys() throws SQLException { + if (currentPs == null) + throw new IllegalStateException("No statement compiled/executed yet."); + return currentPs.getGeneratedKeys(); + } + + public PreparedStatement getPreparedStatement() { + return currentPs; + } + + public String getParsedSql() { + return lastCompiledSql; + } + + public String getOriginalSql() { + return originalSql; + } + + public Set getParameterNames() { + LinkedHashSet 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 path) ---------- */ + + public NamedPreparedStatement setObject(String name, Object value) { + singles.put(name, (ps, i) -> ps.setObject(i, value)); + return this; + } + + 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; + } + + public NamedPreparedStatement setNull(String name, int sqlType) { + singles.put(name, (ps, i) -> ps.setNull(i, sqlType)); + return this; + } + + public NamedPreparedStatement setBoolean(String n, boolean x) { + singles.put(n, (ps, i) -> ps.setBoolean(i, x)); + return this; + } + + public NamedPreparedStatement setByte(String n, byte x) { + singles.put(n, (ps, i) -> ps.setByte(i, x)); + return this; + } + + public NamedPreparedStatement setShort(String n, short x) { + singles.put(n, (ps, i) -> ps.setShort(i, x)); + return this; + } + + public NamedPreparedStatement setInt(String n, int x) { + singles.put(n, (ps, i) -> ps.setInt(i, x)); + return this; + } + + public NamedPreparedStatement setLong(String n, long x) { + singles.put(n, (ps, i) -> ps.setLong(i, x)); + return this; + } + + public NamedPreparedStatement setFloat(String n, float x) { + singles.put(n, (ps, i) -> ps.setFloat(i, x)); + return this; + } + + public NamedPreparedStatement setDouble(String n, double x) { + singles.put(n, (ps, i) -> ps.setDouble(i, x)); + return this; + } + + public NamedPreparedStatement setBigDecimal(String n, BigDecimal x) { + singles.put(n, (ps, i) -> ps.setBigDecimal(i, x)); + return this; + } + + public NamedPreparedStatement setString(String n, String x) { + singles.put(n, (ps, i) -> ps.setString(i, x)); + return this; + } + + public NamedPreparedStatement setBytes(String n, byte[] x) { + singles.put(n, (ps, i) -> ps.setBytes(i, x)); + return this; + } + + public NamedPreparedStatement setDate(String n, Date x) { + singles.put(n, (ps, i) -> ps.setDate(i, x)); + return this; + } + + public NamedPreparedStatement setDate(String n, Date x, Calendar c) { + singles.put(n, (ps, i) -> ps.setDate(i, x, c)); + return this; + } + + public NamedPreparedStatement setTime(String n, Time x) { + singles.put(n, (ps, i) -> ps.setTime(i, x)); + return this; + } + + public NamedPreparedStatement setTime(String n, Time x, Calendar c) { + singles.put(n, (ps, i) -> ps.setTime(i, x, c)); + return this; + } + + public NamedPreparedStatement setTimestamp(String n, Timestamp x) { + singles.put(n, (ps, i) -> ps.setTimestamp(i, x)); + return this; + } + + public NamedPreparedStatement setTimestamp(String n, Timestamp x, Calendar c) { + singles.put(n, (ps, i) -> ps.setTimestamp(i, x, c)); + return this; + } + + public NamedPreparedStatement setArray(String n, Array x) { + singles.put(n, (ps, i) -> ps.setArray(i, x)); + return this; + } + + public NamedPreparedStatement setBlob(String n, Blob x) { + singles.put(n, (ps, i) -> ps.setBlob(i, x)); + return this; + } + + public NamedPreparedStatement setClob(String n, Clob x) { + singles.put(n, (ps, i) -> ps.setClob(i, x)); + return this; + } + + public NamedPreparedStatement setURL(String n, java.net.URL x) { + singles.put(n, (ps, i) -> ps.setURL(i, x)); + return this; + } + + public NamedPreparedStatement setAsciiStream(String n, java.io.InputStream x, int len) { + singles.put(n, (ps, i) -> ps.setAsciiStream(i, x, len)); + return this; + } + + public NamedPreparedStatement setBinaryStream(String n, java.io.InputStream x, int len) { + singles.put(n, (ps, i) -> ps.setBinaryStream(i, x, len)); + return this; + } + + public NamedPreparedStatement setCharacterStream(String n, java.io.Reader r, int len) { + singles.put(n, (ps, i) -> ps.setCharacterStream(i, r, len)); + return this; + } + + public NamedPreparedStatement setList(String name, Collection values) { + lists.put(name, toBinders(values, Types.OTHER, false)); + return this; + } + + public NamedPreparedStatement setList(String name, Object... values) { + lists.put(name, toBinders(Arrays.asList(values), Types.OTHER, false)); + return this; + } + + public NamedPreparedStatement setList(String name, int sqlType, Collection values) { + lists.put(name, toBinders(values, sqlType, true)); + return this; + } + + public NamedPreparedStatement setList(String name, int sqlType, Object... values) { + lists.put(name, toBinders(Arrays.asList(values), sqlType, true)); + return this; + } + + /* ---------- Build & bind (simple path) ---------- */ + + private void buildAndBindSimple() throws SQLException { + StringBuilder sb = new StringBuilder(originalSql.length() + 32); + List 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 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); + } + + int idx = 1; + for (Binder b : bindersInOrder) + b.bind(currentPs, idx++); + } + + /* + * ===================================================================== PLANNED + * / COMPILE-ONCE EXECUTION PATH (Args + Executor) + * ===================================================================== + */ + + /** + * Immutable bag of parameter values. Build these on any thread. They must all + * share the same "shape" (same @list sizes and emptiness) if you want to reuse + * a single Executor (no SQL rebuild). + */ + public static final class Args { + private final Map singles; + private final Map> lists; + + private Args(Map s, Map> l) { + this.singles = Collections.unmodifiableMap(s); + this.lists = unmodifiableListMap(l); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private final Map singles = new HashMap<>(); + private final Map> lists = new HashMap<>(); + + public Builder setNull(String name, int sqlType) { + singles.put(name, (ps, i) -> ps.setNull(i, sqlType)); + return this; + } + + public Builder setObject(String name, Object value) { + singles.put(name, (ps, i) -> ps.setObject(i, value)); + return this; + } + + public Builder 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; + } + + public Builder setBoolean(String n, boolean x) { + singles.put(n, (ps, i) -> ps.setBoolean(i, x)); + return this; + } + + public Builder setByte(String n, byte x) { + singles.put(n, (ps, i) -> ps.setByte(i, x)); + return this; + } + + public Builder setShort(String n, short x) { + singles.put(n, (ps, i) -> ps.setShort(i, x)); + return this; + } + + public Builder setInt(String n, int x) { + singles.put(n, (ps, i) -> ps.setInt(i, x)); + return this; + } + + public Builder setLong(String n, long x) { + singles.put(n, (ps, i) -> ps.setLong(i, x)); + return this; + } + + public Builder setFloat(String n, float x) { + singles.put(n, (ps, i) -> ps.setFloat(i, x)); + return this; + } + + public Builder setDouble(String n, double x) { + singles.put(n, (ps, i) -> ps.setDouble(i, x)); + return this; + } + + public Builder setBigDecimal(String n, BigDecimal x) { + singles.put(n, (ps, i) -> ps.setBigDecimal(i, x)); + return this; + } + + public Builder setString(String n, String x) { + singles.put(n, (ps, i) -> ps.setString(i, x)); + return this; + } + + public Builder setBytes(String n, byte[] x) { + singles.put(n, (ps, i) -> ps.setBytes(i, x)); + return this; + } + + public Builder setDate(String n, Date x) { + singles.put(n, (ps, i) -> ps.setDate(i, x)); + return this; + } + + public Builder setDate(String n, Date x, Calendar c) { + singles.put(n, (ps, i) -> ps.setDate(i, x, c)); + return this; + } + + public Builder setTime(String n, Time x) { + singles.put(n, (ps, i) -> ps.setTime(i, x)); + return this; + } + + public Builder setTime(String n, Time x, Calendar c) { + singles.put(n, (ps, i) -> ps.setTime(i, x, c)); + return this; + } + + public Builder setTimestamp(String n, Timestamp x) { + singles.put(n, (ps, i) -> ps.setTimestamp(i, x)); + return this; + } + + public Builder setTimestamp(String n, Timestamp x, Calendar c) { + singles.put(n, (ps, i) -> ps.setTimestamp(i, x, c)); + return this; + } + + public Builder setArray(String n, Array x) { + singles.put(n, (ps, i) -> ps.setArray(i, x)); + return this; + } + + public Builder setBlob(String n, Blob x) { + singles.put(n, (ps, i) -> ps.setBlob(i, x)); + return this; + } + + public Builder setClob(String n, Clob x) { + singles.put(n, (ps, i) -> ps.setClob(i, x)); + return this; + } + + public Builder setURL(String n, java.net.URL x) { + singles.put(n, (ps, i) -> ps.setURL(i, x)); + return this; + } + + public Builder setAsciiStream(String n, java.io.InputStream x, int len) { + singles.put(n, (ps, i) -> ps.setAsciiStream(i, x, len)); + return this; + } + + public Builder setBinaryStream(String n, java.io.InputStream x, int len) { + singles.put(n, (ps, i) -> ps.setBinaryStream(i, x, len)); + return this; + } + + public Builder setCharacterStream(String n, java.io.Reader r, int len) { + singles.put(n, (ps, i) -> ps.setCharacterStream(i, r, len)); + return this; + } + + public Builder setList(String name, Collection values) { + lists.put(name, toBinders(values, Types.OTHER, false)); + return this; + } + + public Builder setList(String name, Object... values) { + lists.put(name, toBinders(Arrays.asList(values), Types.OTHER, false)); + return this; + } + + public Builder setList(String name, int sqlType, Collection values) { + lists.put(name, toBinders(values, sqlType, true)); + return this; + } + + public Builder setList(String name, int sqlType, Object... values) { + lists.put(name, toBinders(Arrays.asList(values), sqlType, true)); + return this; + } + + public Args build() { + return new Args(new HashMap<>(singles), deepCopyLists(lists)); + } + } + } + + /** + * Executor: compiles SQL once using the "shape" (list sizes / emptiness) of a + * prototype Args, creates one PreparedStatement, and reuses it to execute many + * Args with the same shape. + */ + public static final class Executor implements AutoCloseable { + private final Connection conn; + private final List tokens; + private final String originalSql; + private final EmptyListMode emptyMode; + private final String emptyLiteral; + + private final Integer autoGeneratedKeys; + private final Integer resultSetType; + private final Integer resultSetConcurrency; + + private final Plan plan; // compiled once + private final PreparedStatement ps; // prepared once + + private Executor(Connection conn, List tokens, String originalSql, EmptyListMode mode, + String emptyLiteral, Integer autoGeneratedKeys, Integer resultSetType, Integer resultSetConcurrency, + Args shapeArgs) throws SQLException { + this.conn = conn; + this.tokens = tokens; + this.originalSql = originalSql; + this.emptyMode = mode; + this.emptyLiteral = emptyLiteral; + this.autoGeneratedKeys = autoGeneratedKeys; + this.resultSetType = resultSetType; + this.resultSetConcurrency = resultSetConcurrency; + + this.plan = Plan.compile(tokens, shapeArgs, mode, emptyLiteral); + if (autoGeneratedKeys != null) { + this.ps = conn.prepareStatement(plan.compiledSql, autoGeneratedKeys); + } else if (resultSetType != null && resultSetConcurrency != null) { + this.ps = conn.prepareStatement(plan.compiledSql, resultSetType, resultSetConcurrency); + } else { + this.ps = conn.prepareStatement(plan.compiledSql); + } + } + + /** Build an Executor with the given prototype Args as the shape. */ + public static Executor create(Connection conn, String sql, Args shapeArgs) throws SQLException { + NamedPreparedStatement tmp = new NamedPreparedStatement(conn, sql, null, null, null); + return new Executor(conn, tmp.tokens, tmp.originalSql, EmptyListMode.AS_NULL, "NULL", null, null, null, + shapeArgs); + } + + /** Build an Executor with empty-list toggle and RS options. */ + public static Executor create(Connection conn, String sql, Args shapeArgs, boolean emptyAsNull, + String customEmptyLiteral, Integer autoGeneratedKeys, Integer resultSetType, + Integer resultSetConcurrency) throws SQLException { + NamedPreparedStatement tmp = new NamedPreparedStatement(conn, sql, autoGeneratedKeys, resultSetType, + resultSetConcurrency); + EmptyListMode mode = emptyAsNull ? EmptyListMode.AS_NULL : EmptyListMode.AS_CUSTOM_LITERAL; + String lit = emptyAsNull ? "NULL" : Objects.requireNonNull(customEmptyLiteral, "customEmptyLiteral"); + return new Executor(conn, tmp.tokens, tmp.originalSql, mode, lit, autoGeneratedKeys, resultSetType, + resultSetConcurrency, shapeArgs); + } + + /** Bind a single Args and execute (no JDBC addBatch). */ + public int executeUpdate(Args args) throws SQLException { + plan.validateShape(args); + bind(args); + return ps.executeUpdate(); + } + + public ResultSet executeQuery(Args args) throws SQLException { + plan.validateShape(args); + bind(args); + return ps.executeQuery(); + } + + /** Add to JDBC batch (binds, then ps.addBatch()). */ + public void addBatch(Args args) throws SQLException { + plan.validateShape(args); + bind(args); + ps.addBatch(); + } + + /** Execute the current JDBC batch. */ + public int[] executeBatch() throws SQLException { + return ps.executeBatch(); + } + + /** Convenience: bind+batch all, then executeBatch. */ + public int[] executeBatch(Iterable many) throws SQLException { + for (Args a : many) { + addBatch(a); + } + return ps.executeBatch(); + } + + public PreparedStatement getPreparedStatement() { + return ps; + } + + public String getCompiledSql() { + return plan.compiledSql; + } + + private void bind(Args args) throws SQLException { + ps.clearParameters(); + int idx = 1; + for (BindStep step : plan.steps) { + if (!step.isList) { + Binder b = args.singles.get(step.name); + if (b == null) + throw new IllegalArgumentException("Missing value for :" + step.name); + b.bind(ps, idx++); + } else { + List lst = args.lists.get(step.name); + if (lst == null) + throw new IllegalArgumentException("Missing list for @" + step.name); + if (lst.isEmpty()) { + // No binders for empty → plan generated zero '?' for this step + // (or inserted literal); nothing to bind here. + } else { + for (int k = 0; k < lst.size(); k++) { + lst.get(k).bind(ps, idx++); + } + } + } + } + } + + @Override + public void close() throws SQLException { + ps.close(); + } + } + + /* ---------- Plan: compiled SQL + bind steps ---------- */ + + private static final class Plan { + final String compiledSql; + final List steps; + + private Plan(String sql, List steps) { + this.compiledSql = sql; + this.steps = steps; + } + + static Plan compile(List tokens, Args shape, EmptyListMode mode, String emptyLiteral) { + StringBuilder sb = new StringBuilder(128 + tokens.size() * 4); + ArrayList steps = new ArrayList<>(); + for (Token t : tokens) { + if (t instanceof TextToken tt) { + sb.append(tt.text); + } else { + ParamToken pt = (ParamToken) t; + if (!pt.list) { + // one '?' + sb.append('?'); + steps.add(BindStep.single(pt.name)); + } else { + List lst = shape.lists.get(pt.name); + if (lst == null) + throw new IllegalArgumentException("Prototype Args missing list for @" + pt.name); + if (lst.isEmpty()) { + if (mode == EmptyListMode.AS_NULL) + sb.append("NULL"); + else + sb.append(emptyLiteral); + // No bind steps for this @name occurrence (no '?') + } else { + for (int i = 0; i < lst.size(); i++) { + if (i > 0) + sb.append(','); + sb.append('?'); + } + steps.add(BindStep.list(pt.name)); // bind will consume lst.size() placeholders + } + } + } + } + return new Plan(sb.toString(), steps); + } + + void validateShape(Args args) { + // Ensure each list occurrence is either empty or has same size as prototype for + // that name + // We infer expected sizes by scanning steps and checking the first Args we + // compiled with. + // Simpler approach: recompute name->size from prototype at compile time. + // We'll rely on occurrence-by-occurrence behavior: + // - If a BindStep is list(name), it implies non-empty in prototype; so args + // must also be non-empty and same size. + // - If a list was empty in prototype, there is NO BindStep and NO '?' to bind; + // args must also be empty. + // To implement this, we track expected sizes map during compile; here we + // recompute from steps and prototype shape. + // For simplicity and robustness, we check sizes name-by-name: + Map expected = new HashMap<>(); + for (BindStep s : steps) { + if (s.isList) { + // means prototype had non-empty list for this name; record its size once + expected.putIfAbsent(s.name, -1); // mark needs size check later + } + } + // Fill sizes from prototype-like assumption by examining one Args that created + // the Plan: + // Not directly available here; we can infer on the fly from current args on + // first call, + // but then we'd allow mismatch silently. So we take a practical path: + // We require that for any list name that appears in BindSteps, the size is >0 + // and constant across executions. + for (Map.Entry e : expected.entrySet()) { + List lst = args.lists.get(e.getKey()); + if (lst == null || lst.isEmpty()) + throw new IllegalArgumentException( + "List @" + e.getKey() + " must be non-empty to match Executor shape."); + expected.put(e.getKey(), lst.size()); + } + // Also ensure any list names that were empty in prototype are still empty now: + // We detect these by scanning param names in args that are not in expected. + for (Map.Entry> en : args.lists.entrySet()) { + String name = en.getKey(); + if (!expected.containsKey(name)) { + // This name had empty list in prototype → must be empty now + if (!en.getValue().isEmpty()) + throw new IllegalArgumentException("List @" + name + " must be empty to match Executor shape."); + } + } + // Finally, confirm all expected sizes actually match across all occurrences — + // already implied by name-based size. + } + } + + private static final class BindStep { + final String name; + final boolean isList; + + private BindStep(String n, boolean list) { + this.name = n; + this.isList = list; + } + + static BindStep single(String n) { + return new BindStep(n, false); + } + + static BindStep list(String n) { + return new BindStep(n, true); + } + } + + /* ---------- 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 parse(String sql) { + ArrayList 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 + interface Binder { + void bind(PreparedStatement ps, int index) throws SQLException; + } + + private static List toBinders(Collection values, int sqlType, boolean forceType) { + ArrayList 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> deepCopyLists(Map> src) { + Map> m = new HashMap<>(src.size()); + for (Map.Entry> e : src.entrySet()) { + m.put(e.getKey(), new ArrayList<>(e.getValue())); + } + return m; + } + + private static Map> unmodifiableListMap(Map> src) { + Map> m = new HashMap<>(src.size()); + for (Map.Entry> e : src.entrySet()) { + m.put(e.getKey(), Collections.unmodifiableList(e.getValue())); + } + return Collections.unmodifiableMap(m); + } + + static class ParamSnapshot { + public Map singles; + public Map> lists; + + public ParamSnapshot(Map singles, Map> lists) { super(); this.singles = singles; this.lists = lists; @@ -644,7 +1016,9 @@ public final class NamedPreparedStatement implements AutoCloseable { ParamSnapshot other = (ParamSnapshot) obj; return Objects.equals(lists, other.lists) && Objects.equals(singles, other.singles); } - } - - enum EmptyListMode { AS_NULL, AS_CUSTOM_LITERAL } + } + + enum EmptyListMode { + AS_NULL, AS_CUSTOM_LITERAL + } } -- cgit v1.2.3