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 * Overrides the {@link Database}-wide {@link ResultSetMapper} for this query only. 179 * <p> 180 * This enables per-query inline mapping - for example, projecting an ad-hoc join or computed columns - 181 * without configuring a database-wide mapper. {@link ResultSetMapper} is a functional interface, so a 182 * lambda works: <pre>{@code database.query("SELECT name, COUNT(*) AS total FROM employee GROUP BY name") 183 * .resultSetMapper((ctx, rs, type, ip) -> Optional.of(type.cast(new NameCount(rs.getString(1), rs.getLong(2))))) 184 * .fetchList(NameCount.class);}</pre> 185 * The override applies to every row this query maps (including {@link #fetchStream} rows and DML-returning 186 * results). Other queries on the same {@link Database} are unaffected. Metrics, statement logging, and 187 * exception diagnostics behave identically with an override present. 188 * 189 * @param resultSetMapper the mapper to use for this query, or {@code null} to inherit the database-wide mapper 190 * @return this builder, for chaining 191 * @since 4.5.0 192 */ 193 @NonNull 194 default Query resultSetMapper(@Nullable ResultSetMapper resultSetMapper) { 195 throw new UnsupportedOperationException("resultSetMapper is not supported by this Query implementation"); 196 } 197 198 /** 199 * Overrides the {@link Database}-wide {@link PreparedStatementBinder} for this query only. 200 * <p> 201 * The override receives every non-null parameter for this query - including expanded IN-list elements and 202 * each batch group's parameters. As with the database-wide SPI, {@link SecureParameter} and {@link java.util.Optional} 203 * wrappers are unwrapped by Pyranid <em>before</em> the binder is invoked, so custom binders receive bound-ready 204 * raw values and need no unwrap logic. {@code null} parameters never reach the binder; Pyranid binds them via 205 * {@link java.sql.PreparedStatement#setNull(int, int)} even when an override is present. 206 * <p> 207 * Other queries on the same {@link Database} are unaffected. 208 * 209 * @param preparedStatementBinder the binder to use for this query, or {@code null} to inherit the database-wide binder 210 * @return this builder, for chaining 211 * @since 4.5.0 212 */ 213 @NonNull 214 default Query preparedStatementBinder(@Nullable PreparedStatementBinder preparedStatementBinder) { 215 throw new UnsupportedOperationException("preparedStatementBinder is not supported by this Query implementation"); 216 } 217 218 /** 219 * Customizes the {@link java.sql.PreparedStatement} before execution. 220 * <p> 221 * If called multiple times, the most recent customizer wins. The customizer runs after database-wide statement 222 * settings and this query's {@link #queryTimeout(Duration)}, {@link #fetchSize(Integer)}, and 223 * {@link #maxRows(Integer)} settings, so it can override them when needed. It runs before Pyranid binds parameters. 224 * <p> 225 * For dialect-managed streams, Pyranid may apply driver-specific stream settings after this callback 226 * unless this query explicitly configured {@link #fetchSize(Integer)}. 227 * <p> 228 * For driver-specific cancellation beyond {@link #queryTimeout(Duration)}, application code may capture the 229 * {@link java.sql.PreparedStatement} here and call {@link java.sql.Statement#cancel()} from its own cancellation path. 230 * Cancellation behavior is JDBC-driver-specific. 231 * 232 * @param preparedStatementCustomizer customization callback 233 * @return this builder, for chaining 234 */ 235 @NonNull 236 Query customize(@NonNull PreparedStatementCustomizer preparedStatementCustomizer); 237 238 /** 239 * Acquires a result type token for fetching rows as insertion-ordered {@code Map<String, Object>} instances, 240 * usable anywhere a result type token is accepted. 241 * <p> 242 * Java's type erasure means there is no {@code Map<String, Object>.class} literal - a raw {@code Map.class} 243 * token can only ever infer the raw {@code Map} type at fetch sites. This method returns the same runtime 244 * {@code Map.class} token, statically typed as {@code Class<Map<String, Object>>} so results are properly 245 * parameterized without caller-side casts: 246 * <pre>{@code List<Map<String, Object>> rows = database.query("SELECT * FROM car") 247 * .fetchList(Query.mapRowType());}</pre> 248 * This is safe because Pyranid's default mapping produces {@code LinkedHashMap<String, Object>} rows for this 249 * token. Note that a custom {@link ResultSetMapper} receiving this token observes the raw {@code Map.class}. 250 * 251 * @return a {@code Map<String, Object>} result type token 252 * @since 4.5.0 253 */ 254 @NonNull 255 @SuppressWarnings("unchecked") 256 static Class<Map<String, Object>> mapRowType() { 257 return (Class<Map<String, Object>>) (Class<?>) Map.class; 258 } 259 260 /** 261 * Executes the query and returns a single result. 262 * 263 * @param resultType the type to marshal each row to 264 * @param <T> the result type 265 * @return the single result, or empty if no rows 266 * @throws DatabaseException if more than one row is returned 267 */ 268 @NonNull 269 <T> Optional<T> fetchObject(@NonNull Class<T> resultType); 270 271 /** 272 * Executes the query and returns all results as a list. 273 * 274 * @param resultType the type to marshal each row to 275 * @param <T> the result type 276 * @return list of results (empty if no rows) 277 */ 278 @NonNull 279 <T> List<@Nullable T> fetchList(@NonNull Class<T> resultType); 280 281 /** 282 * Executes the query and provides a {@link Stream} backed by the underlying {@link java.sql.ResultSet}. 283 * <p> 284 * 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. 285 * <p> 286 * JDBC resources are closed automatically when {@code streamFunction} returns (or throws), so the stream must be fully consumed 287 * within that callback. Do not escape the stream from the function. 288 * <p> 289 * The stream must be consumed within the scope of the transaction or connection that created it. 290 * If the stream participates in a Pyranid transaction, it must also be closed by the thread that opened it. 291 * <p> 292 * Supported dialects apply driver-specific streaming setup automatically. PostgreSQL streams use a positive JDBC 293 * fetch size and an autocommit-disabled connection when no Pyranid transaction is active; MySQL streams use 294 * forward-only/read-only statements and the MySQL streaming fetch-size sentinel; MariaDB streams use 295 * forward-only/read-only statements without the MySQL sentinel. Use {@link #fetchSize(Integer)} to override 296 * fetch-size behavior when needed. 297 * 298 * @param resultType the type to marshal each row to 299 * @param streamFunction function that consumes the stream and returns a result 300 * @param <T> the result type 301 * @param <R> the return type 302 * @return the value returned by {@code streamFunction} 303 */ 304 @Nullable 305 <T, R> R fetchStream(@NonNull Class<T> resultType, 306 @NonNull Function<Stream<@Nullable T>, R> streamFunction); 307 308 309 /** 310 * Executes a DML statement (INSERT, UPDATE, DELETE) with no resultset. 311 * 312 * @return the number of rows affected 313 */ 314 @NonNull 315 Long execute(); 316 317 /** 318 * Executes a DML statement and maps a single JDBC-generated key row. 319 * <p> 320 * This uses JDBC {@link java.sql.Statement#RETURN_GENERATED_KEYS}. It is intended for database-generated values such 321 * as identity/auto-increment primary keys. If your SQL returns rows directly via database syntax such as PostgreSQL 322 * {@code RETURNING} or SQL Server {@code OUTPUT}, use {@link #executeForObject(Class)} instead. 323 * <p> 324 * For SQL Server multi-row identity inserts, use {@code OUTPUT} with {@link #executeForList(Class)} instead of JDBC 325 * generated keys. For MySQL {@code INSERT ... ON DUPLICATE KEY UPDATE}, use the {@code LAST_INSERT_ID(id)} idiom if 326 * you need the existing row's ID returned on the update path. 327 * <p> 328 * Oracle requires explicit generated-key column names; use {@link #executeReturningGeneratedKey(Class, String...)} 329 * instead. 330 * 331 * @param resultType the type to marshal the generated-key row to 332 * @param <T> the result type 333 * @return the single generated key row, or empty if the driver returns no generated keys 334 * @throws DatabaseException if more than one generated-key row is returned 335 * @since 4.2.0 336 */ 337 @NonNull 338 default <T> Optional<T> executeReturningGeneratedKey(@NonNull Class<T> resultType) { 339 throw new UnsupportedOperationException("executeReturningGeneratedKey is not supported by this Query implementation"); 340 } 341 342 /** 343 * Executes a DML statement and maps a single JDBC-generated key row. 344 * <p> 345 * This uses JDBC {@link java.sql.Connection#prepareStatement(String, String[])} with the supplied key column names. 346 * Some drivers require column names to return generated keys for specific columns, especially when more than one 347 * generated value is available. If {@code keyColumnNames} is empty, this behaves like 348 * {@link #executeReturningGeneratedKey(Class)}, except for dialects such as Oracle which require explicit names. 349 * <p> 350 * For SQL Server multi-row identity inserts, use {@code OUTPUT} with {@link #executeForList(Class)} instead of JDBC 351 * generated keys. For MySQL {@code INSERT ... ON DUPLICATE KEY UPDATE}, use the {@code LAST_INSERT_ID(id)} idiom if 352 * you need the existing row's ID returned on the update path. 353 * 354 * @param resultType the type to marshal the generated-key row to 355 * @param keyColumnNames generated-key column names requested from the driver 356 * @param <T> the result type 357 * @return the single generated key row, or empty if the driver returns no generated keys 358 * @throws DatabaseException if more than one generated-key row is returned 359 * @since 4.2.0 360 */ 361 @NonNull 362 default <T> Optional<T> executeReturningGeneratedKey(@NonNull Class<T> resultType, 363 @NonNull String @NonNull ... keyColumnNames) { 364 throw new UnsupportedOperationException("executeReturningGeneratedKey is not supported by this Query implementation"); 365 } 366 367 /** 368 * Executes a DML statement and maps all JDBC-generated key rows. 369 * <p> 370 * This uses JDBC {@link java.sql.Statement#RETURN_GENERATED_KEYS}. It is intended for database-generated values such 371 * as identity/auto-increment primary keys. If your SQL returns rows directly via database syntax such as PostgreSQL 372 * {@code RETURNING} or SQL Server {@code OUTPUT}, use {@link #executeForList(Class)} instead. 373 * <p> 374 * For SQL Server multi-row identity inserts, use {@code OUTPUT} with {@link #executeForList(Class)} instead of JDBC 375 * generated keys. For MySQL {@code INSERT ... ON DUPLICATE KEY UPDATE}, use the {@code LAST_INSERT_ID(id)} idiom if 376 * you need the existing row's ID returned on the update path. 377 * <p> 378 * Oracle requires explicit generated-key column names; use {@link #executeReturningGeneratedKeys(Class, String...)} 379 * instead. 380 * 381 * @param resultType the type to marshal each generated-key row to 382 * @param <T> the result type 383 * @return list of generated key rows 384 * @since 4.2.0 385 */ 386 @NonNull 387 default <T> List<@Nullable T> executeReturningGeneratedKeys(@NonNull Class<T> resultType) { 388 throw new UnsupportedOperationException("executeReturningGeneratedKeys is not supported by this Query implementation"); 389 } 390 391 /** 392 * Executes a DML statement and maps all JDBC-generated key rows. 393 * <p> 394 * This uses JDBC {@link java.sql.Connection#prepareStatement(String, String[])} with the supplied key column names. 395 * Some drivers require column names to return generated keys for specific columns, especially when more than one 396 * generated value is available. If {@code keyColumnNames} is empty, this behaves like 397 * {@link #executeReturningGeneratedKeys(Class)}, except for dialects such as Oracle which require explicit names. 398 * <p> 399 * For SQL Server multi-row identity inserts, use {@code OUTPUT} with {@link #executeForList(Class)} instead of JDBC 400 * generated keys. For MySQL {@code INSERT ... ON DUPLICATE KEY UPDATE}, use the {@code LAST_INSERT_ID(id)} idiom if 401 * you need the existing row's ID returned on the update path. 402 * 403 * @param resultType the type to marshal each generated-key row to 404 * @param keyColumnNames generated-key column names requested from the driver 405 * @param <T> the result type 406 * @return list of generated key rows 407 * @since 4.2.0 408 */ 409 @NonNull 410 default <T> List<@Nullable T> executeReturningGeneratedKeys(@NonNull Class<T> resultType, 411 @NonNull String @NonNull ... keyColumnNames) { 412 throw new UnsupportedOperationException("executeReturningGeneratedKeys is not supported by this Query implementation"); 413 } 414 415 /** 416 * Executes a DML statement in batch over groups of named parameters. 417 * <p> 418 * Any parameters already bound on this {@code Query} apply to all groups; group values override them. 419 * Each group must provide a complete set of parameter values after merging; groups must be non-null and 420 * expand to the same number of JDBC parameters (for example, IN-list sizes must match). 421 * 422 * @param parameterGroups groups of named parameter values (without the leading {@code :}) 423 * @return the number of rows affected by the SQL statement per-group 424 */ 425 @NonNull 426 List<Long> executeBatch(@NonNull List<@NonNull Map<@NonNull String, @Nullable Object>> parameterGroups); 427 428 /** 429 * Executes a DML statement that returns a single row (for example, with PostgreSQL/SQLite {@code RETURNING}, 430 * MariaDB {@code INSERT ... RETURNING}, or SQL Server {@code OUTPUT}). 431 * 432 * @param resultType the type to marshal the row to 433 * @param <T> the result type 434 * @return the single result, or empty if no rows 435 * @throws DatabaseException if more than one row is returned 436 */ 437 @NonNull 438 <T> Optional<T> executeForObject(@NonNull Class<T> resultType); 439 440 /** 441 * Executes a DML statement that returns multiple rows (for example, with PostgreSQL/SQLite {@code RETURNING}, 442 * MariaDB {@code INSERT ... RETURNING}, or SQL Server {@code OUTPUT}). 443 * 444 * @param resultType the type to marshal each row to 445 * @param <T> the result type 446 * @return list of results 447 */ 448 @NonNull 449 <T> List<@Nullable T> executeForList(@NonNull Class<T> resultType); 450}