001/* 002 * Copyright 2015-2022 Transmogrify LLC, 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.pyranid; 018 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.NotThreadSafe; 023import java.time.Duration; 024import java.util.List; 025import java.util.Map; 026import java.util.Optional; 027import java.util.function.Function; 028import java.util.stream.Stream; 029 030/** 031 * Fluent builder for SQL statements. 032 * <p> 033 * Obtain instances via {@link Database#query(String)}. 034 * Positional parameters via {@code ?} are not supported; use named parameters (e.g. {@code :id}) and {@link #bind(String, Object)}. 035 * Parameter-looking text inside SQL string literals, quoted identifiers, comments, PostgreSQL dollar-quoted strings, 036 * and SQL Server-style bracket-quoted identifiers is ignored. PostgreSQL JSONB/hstore {@code ?}, {@code ?|}, and 037 * {@code ?&} operators are supported and are escaped automatically for pgjdbc when the {@link Database} is configured 038 * or detected as PostgreSQL. 039 * <p> 040 * Example usage: 041 * <pre>{@code 042 * // Query returning one row 043 * Optional<Employee> employee = database.query("SELECT * FROM employee WHERE id = :id") 044 * .bind("id", 42) 045 * .fetchObject(Employee.class); 046 * 047 * // Query returning multiple rows 048 * List<Employee> employees = database.query("SELECT * FROM employee WHERE dept = :dept") 049 * .bind("dept", "Engineering") 050 * .fetchList(Employee.class); 051 * 052 * // DML with no result 053 * long rowsAffected = database.query("UPDATE employee SET active = :active WHERE id = :id") 054 * .bind("id", 42) 055 * .bind("active", false) 056 * .execute(); 057 * 058 * // DML with RETURNING clause 059 * Optional<Employee> updated = database.query("UPDATE employee SET salary = :salary WHERE id = :id RETURNING *") 060 * .bind("id", 42) 061 * .bind("salary", new BigDecimal("150000")) 062 * .executeForObject(Employee.class); 063 * }</pre> 064 * <p> 065 * Implementations of this interface are intended for use by a single thread. 066 * 067 * @author <a href="https://www.revetkn.com">Mark Allen</a> 068 * @see Database#query(String) 069 * @since 4.0.0 070 */ 071@NotThreadSafe 072public interface Query { 073 /** 074 * Binds a named parameter to a value. 075 * 076 * @param name the parameter name (without the leading {@code :}) 077 * @param value the value to bind (may be {@code null}). Raw {@link java.util.Collection} and array 078 * values are not expanded; use {@link Parameters#inList(java.util.Collection)}, 079 * {@link Parameters#sqlArrayOf(String, Object[])}, or {@link Parameters#arrayOf(Class, Object)} 080 * as appropriate. 081 * @return this builder, for chaining 082 */ 083 @NonNull 084 Query bind(@NonNull String name, 085 @Nullable Object value); 086 087 /** 088 * Binds all entries from the given map as named parameters. 089 * 090 * @param parameters map of parameter names to values 091 * @return this builder, for chaining 092 */ 093 @NonNull 094 Query bindAll(@NonNull Map<@NonNull String, @Nullable Object> parameters); 095 096 /** 097 * Associates an identifier with this query for logging/diagnostics. 098 * <p> 099 * If not called, a default ID will be generated. 100 * 101 * @param id the identifier 102 * @return this builder, for chaining 103 */ 104 @NonNull 105 Query id(@Nullable Object id); 106 107 /** 108 * Configures the JDBC query timeout for this query. 109 * <p> 110 * This maps to {@link java.sql.Statement#setQueryTimeout(int)}. {@code null} leaves the timeout unset so any 111 * {@link Database.Builder#queryTimeout(Duration)} default applies. {@link Duration#ZERO} disables the JDBC timeout. 112 * Positive sub-second durations are rounded up to one second because JDBC accepts whole seconds. 113 * 114 * @param queryTimeout timeout to apply, or {@code null} to inherit the database default 115 * @return this builder, for chaining 116 * @since 4.2.0 117 */ 118 @NonNull 119 default Query queryTimeout(@Nullable Duration queryTimeout) { 120 throw new UnsupportedOperationException("queryTimeout is not supported by this Query implementation"); 121 } 122 123 /** 124 * Configures the JDBC fetch size for this query. 125 * <p> 126 * This maps to {@link java.sql.Statement#setFetchSize(int)}. {@code null} leaves the fetch size unset so any 127 * {@link Database.Builder#fetchSize(Integer)} default applies. A value of {@code 0} uses the driver's default 128 * fetch-size behavior. 129 * 130 * @param fetchSize fetch size to apply, or {@code null} to inherit the database default 131 * @return this builder, for chaining 132 * @since 4.2.0 133 */ 134 @NonNull 135 default Query fetchSize(@Nullable Integer fetchSize) { 136 throw new UnsupportedOperationException("fetchSize is not supported by this Query implementation"); 137 } 138 139 /** 140 * Configures the JDBC maximum row count for this query. 141 * <p> 142 * This maps to {@link java.sql.Statement#setMaxRows(int)}. {@code null} leaves the maximum row count unset so any 143 * {@link Database.Builder#maxRows(Integer)} default applies. A value of {@code 0} disables the JDBC row limit. 144 * 145 * @param maxRows maximum rows to apply, or {@code null} to inherit the database default 146 * @return this builder, for chaining 147 * @since 4.2.0 148 */ 149 @NonNull 150 default Query maxRows(@Nullable Integer maxRows) { 151 throw new UnsupportedOperationException("maxRows is not supported by this Query implementation"); 152 } 153 154 /** 155 * Configures the maximum number of parameter groups to send in each JDBC batch execution. 156 * <p> 157 * This setting applies only to {@link #executeBatch(List)}. {@code null} leaves batch execution unchunked, 158 * preserving the default behavior of sending all parameter groups in one JDBC batch. A positive value causes 159 * Pyranid to execute multiple JDBC batches as needed and flatten the returned update counts in input order. If this 160 * setting is specified and a non-batch terminal operation is invoked, Pyranid throws {@link IllegalStateException}. 161 * <p> 162 * Chunking is performed by Pyranid. JDBC drivers still execute each chunk as a normal JDBC batch. 163 * If a later chunk fails outside an explicit transaction, earlier chunks may already be committed depending on 164 * autocommit and driver behavior. Wrap chunked batches in {@link Database#transaction(TransactionalOperation)} when 165 * all-or-nothing behavior is required. 166 * 167 * @param batchChunkSize maximum parameter groups per JDBC batch execution, or {@code null} to execute one batch 168 * @return this builder, for chaining 169 * @throws IllegalArgumentException if {@code batchChunkSize} is less than or equal to {@code 0} 170 * @since 4.2.0 171 */ 172 @NonNull 173 default Query batchChunkSize(@Nullable Integer batchChunkSize) { 174 throw new UnsupportedOperationException("batchChunkSize is not supported by this Query implementation"); 175 } 176 177 /** 178 * Customizes the {@link java.sql.PreparedStatement} before execution. 179 * <p> 180 * If called multiple times, the most recent customizer wins. The customizer runs after database-wide statement 181 * settings and this query's {@link #queryTimeout(Duration)}, {@link #fetchSize(Integer)}, and 182 * {@link #maxRows(Integer)} settings, so it can override them when needed. 183 * <p> 184 * For dialect-managed streams, Pyranid may apply driver-specific stream settings after this callback 185 * unless this query explicitly configured {@link #fetchSize(Integer)}. 186 * <p> 187 * For driver-specific cancellation beyond {@link #queryTimeout(Duration)}, application code may capture the 188 * {@link java.sql.PreparedStatement} here and call {@link java.sql.Statement#cancel()} from its own cancellation path. 189 * Cancellation behavior is JDBC-driver-specific. 190 * 191 * @param preparedStatementCustomizer customization callback 192 * @return this builder, for chaining 193 */ 194 @NonNull 195 Query customize(@NonNull PreparedStatementCustomizer preparedStatementCustomizer); 196 197 /** 198 * Executes the query and returns a single result. 199 * 200 * @param resultType the type to marshal each row to 201 * @param <T> the result type 202 * @return the single result, or empty if no rows 203 * @throws DatabaseException if more than one row is returned 204 */ 205 @NonNull 206 <T> Optional<T> fetchObject(@NonNull Class<T> resultType); 207 208 /** 209 * Executes the query and returns all results as a list. 210 * 211 * @param resultType the type to marshal each row to 212 * @param <T> the result type 213 * @return list of results (empty if no rows) 214 */ 215 @NonNull 216 <T> List<@Nullable T> fetchList(@NonNull Class<T> resultType); 217 218 /** 219 * Executes the query and provides a {@link Stream} backed by the underlying {@link java.sql.ResultSet}. 220 * <p> 221 * This approach is useful for processing very large resultsets (e.g. millions of rows), where it's impractical to load all rows into memory at once. 222 * <p> 223 * JDBC resources are closed automatically when {@code streamFunction} returns (or throws), so the stream must be fully consumed 224 * within that callback. Do not escape the stream from the function. 225 * <p> 226 * The stream must be consumed within the scope of the transaction or connection that created it. 227 * If the stream participates in a Pyranid transaction, it must also be closed by the thread that opened it. 228 * <p> 229 * Supported dialects apply driver-specific streaming setup automatically. PostgreSQL streams use a positive JDBC 230 * fetch size and an autocommit-disabled connection when no Pyranid transaction is active; MySQL streams use 231 * forward-only/read-only statements and the MySQL streaming fetch-size sentinel; MariaDB streams use 232 * forward-only/read-only statements without the MySQL sentinel. Use {@link #fetchSize(Integer)} to override 233 * fetch-size behavior when needed. 234 * 235 * @param resultType the type to marshal each row to 236 * @param streamFunction function that consumes the stream and returns a result 237 * @param <T> the result type 238 * @param <R> the return type 239 * @return the value returned by {@code streamFunction} 240 */ 241 @Nullable 242 <T, R> R fetchStream(@NonNull Class<T> resultType, 243 @NonNull Function<Stream<@Nullable T>, R> streamFunction); 244 245 246 /** 247 * Executes a DML statement (INSERT, UPDATE, DELETE) with no resultset. 248 * 249 * @return the number of rows affected 250 */ 251 @NonNull 252 Long execute(); 253 254 /** 255 * Executes a DML statement and maps a single JDBC-generated key row. 256 * <p> 257 * This uses JDBC {@link java.sql.Statement#RETURN_GENERATED_KEYS}. It is intended for database-generated values such 258 * as identity/auto-increment primary keys. If your SQL returns rows directly via database syntax such as PostgreSQL 259 * {@code RETURNING} or SQL Server {@code OUTPUT}, use {@link #executeForObject(Class)} instead. 260 * <p> 261 * For SQL Server multi-row identity inserts, use {@code OUTPUT} with {@link #executeForList(Class)} instead of JDBC 262 * generated keys. For MySQL {@code INSERT ... ON DUPLICATE KEY UPDATE}, use the {@code LAST_INSERT_ID(id)} idiom if 263 * you need the existing row's ID returned on the update path. 264 * <p> 265 * Oracle requires explicit generated-key column names; use {@link #executeReturningGeneratedKey(Class, String...)} 266 * instead. 267 * 268 * @param resultType the type to marshal the generated-key row to 269 * @param <T> the result type 270 * @return the single generated key row, or empty if the driver returns no generated keys 271 * @throws DatabaseException if more than one generated-key row is returned 272 * @since 4.2.0 273 */ 274 @NonNull 275 default <T> Optional<T> executeReturningGeneratedKey(@NonNull Class<T> resultType) { 276 throw new UnsupportedOperationException("executeReturningGeneratedKey is not supported by this Query implementation"); 277 } 278 279 /** 280 * Executes a DML statement and maps a single JDBC-generated key row. 281 * <p> 282 * This uses JDBC {@link java.sql.Connection#prepareStatement(String, String[])} with the supplied key column names. 283 * Some drivers require column names to return generated keys for specific columns, especially when more than one 284 * generated value is available. If {@code keyColumnNames} is empty, this behaves like 285 * {@link #executeReturningGeneratedKey(Class)}, except for dialects such as Oracle which require explicit names. 286 * <p> 287 * For SQL Server multi-row identity inserts, use {@code OUTPUT} with {@link #executeForList(Class)} instead of JDBC 288 * generated keys. For MySQL {@code INSERT ... ON DUPLICATE KEY UPDATE}, use the {@code LAST_INSERT_ID(id)} idiom if 289 * you need the existing row's ID returned on the update path. 290 * 291 * @param resultType the type to marshal the generated-key row to 292 * @param keyColumnNames generated-key column names requested from the driver 293 * @param <T> the result type 294 * @return the single generated key row, or empty if the driver returns no generated keys 295 * @throws DatabaseException if more than one generated-key row is returned 296 * @since 4.2.0 297 */ 298 @NonNull 299 default <T> Optional<T> executeReturningGeneratedKey(@NonNull Class<T> resultType, 300 @NonNull String @NonNull ... keyColumnNames) { 301 throw new UnsupportedOperationException("executeReturningGeneratedKey is not supported by this Query implementation"); 302 } 303 304 /** 305 * Executes a DML statement and maps all JDBC-generated key rows. 306 * <p> 307 * This uses JDBC {@link java.sql.Statement#RETURN_GENERATED_KEYS}. It is intended for database-generated values such 308 * as identity/auto-increment primary keys. If your SQL returns rows directly via database syntax such as PostgreSQL 309 * {@code RETURNING} or SQL Server {@code OUTPUT}, use {@link #executeForList(Class)} instead. 310 * <p> 311 * For SQL Server multi-row identity inserts, use {@code OUTPUT} with {@link #executeForList(Class)} instead of JDBC 312 * generated keys. For MySQL {@code INSERT ... ON DUPLICATE KEY UPDATE}, use the {@code LAST_INSERT_ID(id)} idiom if 313 * you need the existing row's ID returned on the update path. 314 * <p> 315 * Oracle requires explicit generated-key column names; use {@link #executeReturningGeneratedKeys(Class, String...)} 316 * instead. 317 * 318 * @param resultType the type to marshal each generated-key row to 319 * @param <T> the result type 320 * @return list of generated key rows 321 * @since 4.2.0 322 */ 323 @NonNull 324 default <T> List<@Nullable T> executeReturningGeneratedKeys(@NonNull Class<T> resultType) { 325 throw new UnsupportedOperationException("executeReturningGeneratedKeys is not supported by this Query implementation"); 326 } 327 328 /** 329 * Executes a DML statement and maps all JDBC-generated key rows. 330 * <p> 331 * This uses JDBC {@link java.sql.Connection#prepareStatement(String, String[])} with the supplied key column names. 332 * Some drivers require column names to return generated keys for specific columns, especially when more than one 333 * generated value is available. If {@code keyColumnNames} is empty, this behaves like 334 * {@link #executeReturningGeneratedKeys(Class)}, except for dialects such as Oracle which require explicit names. 335 * <p> 336 * For SQL Server multi-row identity inserts, use {@code OUTPUT} with {@link #executeForList(Class)} instead of JDBC 337 * generated keys. For MySQL {@code INSERT ... ON DUPLICATE KEY UPDATE}, use the {@code LAST_INSERT_ID(id)} idiom if 338 * you need the existing row's ID returned on the update path. 339 * 340 * @param resultType the type to marshal each generated-key row to 341 * @param keyColumnNames generated-key column names requested from the driver 342 * @param <T> the result type 343 * @return list of generated key rows 344 * @since 4.2.0 345 */ 346 @NonNull 347 default <T> List<@Nullable T> executeReturningGeneratedKeys(@NonNull Class<T> resultType, 348 @NonNull String @NonNull ... keyColumnNames) { 349 throw new UnsupportedOperationException("executeReturningGeneratedKeys is not supported by this Query implementation"); 350 } 351 352 /** 353 * Executes a DML statement in batch over groups of named parameters. 354 * <p> 355 * Any parameters already bound on this {@code Query} apply to all groups; group values override them. 356 * Each group must provide a complete set of parameter values after merging; groups must be non-null and 357 * expand to the same number of JDBC parameters (for example, IN-list sizes must match). 358 * 359 * @param parameterGroups groups of named parameter values (without the leading {@code :}) 360 * @return the number of rows affected by the SQL statement per-group 361 */ 362 @NonNull 363 List<Long> executeBatch(@NonNull List<@NonNull Map<@NonNull String, @Nullable Object>> parameterGroups); 364 365 /** 366 * Executes a DML statement that returns a single row (for example, with PostgreSQL/SQLite/MariaDB 367 * {@code RETURNING} or SQL Server {@code OUTPUT}). 368 * 369 * @param resultType the type to marshal the row to 370 * @param <T> the result type 371 * @return the single result, or empty if no rows 372 * @throws DatabaseException if more than one row is returned 373 */ 374 @NonNull 375 <T> Optional<T> executeForObject(@NonNull Class<T> resultType); 376 377 /** 378 * Executes a DML statement that returns multiple rows (for example, with PostgreSQL/SQLite/MariaDB 379 * {@code RETURNING} or SQL Server {@code OUTPUT}). 380 * 381 * @param resultType the type to marshal each row to 382 * @param <T> the result type 383 * @return list of results 384 */ 385 @NonNull 386 <T> List<@Nullable T> executeForList(@NonNull Class<T> resultType); 387}