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. For DuckDB, brackets are parsed as list/subscript syntax, 037 * so list literals can contain named parameters. PostgreSQL JSONB/hstore {@code ?}, {@code ?|}, and {@code ?&} 038 * operators are supported and are escaped automatically for pgjdbc when the {@link Database} is configured or detected 039 * as PostgreSQL. 040 * <p> 041 * Example usage: 042 * <pre>{@code 043 * // Query returning one row 044 * Optional<Employee> employee = database.query("SELECT * FROM employee WHERE id = :id") 045 * .bind("id", 42) 046 * .fetchObject(Employee.class); 047 * 048 * // Query returning multiple rows 049 * List<Employee> employees = database.query("SELECT * FROM employee WHERE dept = :dept") 050 * .bind("dept", "Engineering") 051 * .fetchList(Employee.class); 052 * 053 * // DML with no result 054 * long rowsAffected = database.query("UPDATE employee SET active = :active WHERE id = :id") 055 * .bind("id", 42) 056 * .bind("active", false) 057 * .execute(); 058 * 059 * // DML with RETURNING clause 060 * Optional<Employee> updated = database.query("UPDATE employee SET salary = :salary WHERE id = :id RETURNING *") 061 * .bind("id", 42) 062 * .bind("salary", new BigDecimal("150000")) 063 * .executeForObject(Employee.class); 064 * }</pre> 065 * <p> 066 * Implementations of this interface are intended for use by a single thread. 067 * 068 * @author <a href="https://www.revetkn.com">Mark Allen</a> 069 * @see Database#query(String) 070 * @since 4.0.0 071 */ 072@NotThreadSafe 073public interface Query { 074 /** 075 * Binds a named parameter to a value. 076 * 077 * @param name the parameter name (without the leading {@code :}) 078 * @param value the value to bind (may be {@code null}). Raw {@link java.util.Collection} and array 079 * values are not expanded; use {@link Parameters#inList(java.util.Collection)}, 080 * {@link Parameters#sqlArrayOf(String, Object[])}, or {@link Parameters#arrayOf(Class, Object)} 081 * as appropriate. 082 * @return this builder, for chaining 083 */ 084 @NonNull 085 Query bind(@NonNull String name, 086 @Nullable Object value); 087 088 /** 089 * Binds all entries from the given map as named parameters. 090 * 091 * @param parameters map of parameter names to values 092 * @return this builder, for chaining 093 */ 094 @NonNull 095 Query bindAll(@NonNull Map<@NonNull String, @Nullable Object> parameters); 096 097 /** 098 * Associates an identifier with this query for logging/diagnostics. 099 * <p> 100 * If not called, a default ID will be generated. 101 * 102 * @param id the identifier 103 * @return this builder, for chaining 104 */ 105 @NonNull 106 Query id(@Nullable Object id); 107 108 /** 109 * Configures the JDBC query timeout for this query. 110 * <p> 111 * This maps to {@link java.sql.Statement#setQueryTimeout(int)}. {@code null} leaves the timeout unset so any 112 * {@link Database.Builder#queryTimeout(Duration)} default applies. {@link Duration#ZERO} disables the JDBC timeout. 113 * Positive sub-second durations are rounded up to one second because JDBC accepts whole seconds. 114 * 115 * @param queryTimeout timeout to apply, or {@code null} to inherit the database default 116 * @return this builder, for chaining 117 * @since 4.2.0 118 */ 119 @NonNull 120 default Query queryTimeout(@Nullable Duration queryTimeout) { 121 throw new UnsupportedOperationException("queryTimeout is not supported by this Query implementation"); 122 } 123 124 /** 125 * Configures the JDBC fetch size for this query. 126 * <p> 127 * This maps to {@link java.sql.Statement#setFetchSize(int)}. {@code null} leaves the fetch size unset so any 128 * {@link Database.Builder#fetchSize(Integer)} default applies. A value of {@code 0} uses the driver's default 129 * fetch-size behavior. 130 * 131 * @param fetchSize fetch size to apply, or {@code null} to inherit the database default 132 * @return this builder, for chaining 133 * @since 4.2.0 134 */ 135 @NonNull 136 default Query fetchSize(@Nullable Integer fetchSize) { 137 throw new UnsupportedOperationException("fetchSize is not supported by this Query implementation"); 138 } 139 140 /** 141 * Configures the JDBC maximum row count for this query. 142 * <p> 143 * This maps to {@link java.sql.Statement#setMaxRows(int)}. {@code null} leaves the maximum row count unset so any 144 * {@link Database.Builder#maxRows(Integer)} default applies. A value of {@code 0} disables the JDBC row limit. 145 * 146 * @param maxRows maximum rows to apply, or {@code null} to inherit the database default 147 * @return this builder, for chaining 148 * @since 4.2.0 149 */ 150 @NonNull 151 default Query maxRows(@Nullable Integer maxRows) { 152 throw new UnsupportedOperationException("maxRows is not supported by this Query implementation"); 153 } 154 155 /** 156 * Configures the maximum number of parameter groups to send in each JDBC batch execution. 157 * <p> 158 * This setting applies only to {@link #executeBatch(List)}. {@code null} leaves batch execution unchunked, 159 * preserving the default behavior of sending all parameter groups in one JDBC batch. A positive value causes 160 * Pyranid to execute multiple JDBC batches as needed and flatten the returned update counts in input order. If this 161 * setting is specified and a non-batch terminal operation is invoked, Pyranid throws {@link IllegalStateException}. 162 * <p> 163 * Chunking is performed by Pyranid. JDBC drivers still execute each chunk as a normal JDBC batch. 164 * If a later chunk fails outside an explicit transaction, earlier chunks may already be committed depending on 165 * autocommit and driver behavior. Wrap chunked batches in {@link Database#transaction(TransactionalOperation)} when 166 * all-or-nothing behavior is required. 167 * 168 * @param batchChunkSize maximum parameter groups per JDBC batch execution, or {@code null} to execute one batch 169 * @return this builder, for chaining 170 * @throws IllegalArgumentException if {@code batchChunkSize} is less than or equal to {@code 0} 171 * @since 4.2.0 172 */ 173 @NonNull 174 default Query batchChunkSize(@Nullable Integer batchChunkSize) { 175 throw new UnsupportedOperationException("batchChunkSize is not supported by this Query implementation"); 176 } 177 178 /** 179 * Overrides the {@link Database}-wide {@link ResultSetMapper} for this query only. 180 * <p> 181 * This enables per-query inline mapping - for example, projecting an ad-hoc join or computed columns - 182 * without configuring a database-wide mapper. {@link ResultSetMapper} is a functional interface, so a 183 * lambda works: <pre>{@code database.query("SELECT name, COUNT(*) AS total FROM employee GROUP BY name") 184 * .resultSetMapper((ctx, rs, type, ip) -> Optional.of(type.cast(new NameCount(rs.getString(1), rs.getLong(2))))) 185 * .fetchList(NameCount.class);}</pre> 186 * The override applies to every row this query maps (including {@link #fetchStream} rows and DML-returning 187 * results). Other queries on the same {@link Database} are unaffected. Metrics, statement logging, and 188 * exception diagnostics behave identically with an override present. 189 * 190 * @param resultSetMapper the mapper to use for this query, or {@code null} to inherit the database-wide mapper 191 * @return this builder, for chaining 192 * @since 4.5.0 193 */ 194 @NonNull 195 default Query resultSetMapper(@Nullable ResultSetMapper resultSetMapper) { 196 throw new UnsupportedOperationException("resultSetMapper is not supported by this Query implementation"); 197 } 198 199 /** 200 * Overrides the {@link Database}-wide {@link PreparedStatementBinder} for this query only. 201 * <p> 202 * The override receives every non-null parameter for this query - including expanded IN-list elements and 203 * each batch group's parameters. As with the database-wide SPI, {@link SecureParameter} and {@link java.util.Optional} 204 * wrappers are unwrapped by Pyranid <em>before</em> the binder is invoked, so custom binders receive bound-ready 205 * raw values and need no unwrap logic. {@code null} parameters never reach the binder; Pyranid binds them via 206 * {@link java.sql.PreparedStatement#setNull(int, int)} even when an override is present. 207 * <p> 208 * Other queries on the same {@link Database} are unaffected. 209 * 210 * @param preparedStatementBinder the binder to use for this query, or {@code null} to inherit the database-wide binder 211 * @return this builder, for chaining 212 * @since 4.5.0 213 */ 214 @NonNull 215 default Query preparedStatementBinder(@Nullable PreparedStatementBinder preparedStatementBinder) { 216 throw new UnsupportedOperationException("preparedStatementBinder is not supported by this Query implementation"); 217 } 218 219 /** 220 * Customizes the {@link java.sql.PreparedStatement} before execution. 221 * <p> 222 * If called multiple times, the most recent customizer wins. The customizer runs after database-wide statement 223 * settings and this query's {@link #queryTimeout(Duration)}, {@link #fetchSize(Integer)}, and 224 * {@link #maxRows(Integer)} settings, so it can override them when needed. It runs before Pyranid binds parameters. 225 * <p> 226 * For dialect-managed streams, Pyranid may apply driver-specific stream settings after this callback 227 * unless this query explicitly configured {@link #fetchSize(Integer)}. 228 * <p> 229 * For driver-specific cancellation beyond {@link #queryTimeout(Duration)}, application code may capture the 230 * {@link java.sql.PreparedStatement} here and call {@link java.sql.Statement#cancel()} from its own cancellation path. 231 * Cancellation behavior is JDBC-driver-specific. 232 * 233 * @param preparedStatementCustomizer customization callback 234 * @return this builder, for chaining 235 */ 236 @NonNull 237 Query customize(@NonNull PreparedStatementCustomizer preparedStatementCustomizer); 238 239 /** 240 * Acquires a result type token for fetching rows as insertion-ordered {@code Map<String, Object>} instances, 241 * usable anywhere a result type token is accepted. 242 * <p> 243 * Java's type erasure means there is no {@code Map<String, Object>.class} literal - a raw {@code Map.class} 244 * token can only ever infer the raw {@code Map} type at fetch sites. This method returns the same runtime 245 * {@code Map.class} token, statically typed as {@code Class<Map<String, Object>>} so results are properly 246 * parameterized without caller-side casts: 247 * <pre>{@code List<Map<String, Object>> rows = database.query("SELECT * FROM car") 248 * .fetchList(Query.mapRowType());}</pre> 249 * This is safe because Pyranid's default mapping produces {@code LinkedHashMap<String, Object>} rows for this 250 * token. Note that a custom {@link ResultSetMapper} receiving this token observes the raw {@code Map.class}. 251 * 252 * @return a {@code Map<String, Object>} result type token 253 * @since 4.5.0 254 */ 255 @NonNull 256 @SuppressWarnings("unchecked") 257 static Class<Map<String, Object>> mapRowType() { 258 return (Class<Map<String, Object>>) (Class<?>) Map.class; 259 } 260 261 /** 262 * Executes the query and returns a single result. 263 * 264 * @param resultType the type to marshal each row to 265 * @param <T> the result type 266 * @return the single result, or empty if no rows 267 * @throws DatabaseException if more than one row is returned 268 */ 269 @NonNull 270 <T> Optional<T> fetchObject(@NonNull Class<T> resultType); 271 272 /** 273 * Executes the query and returns all results as a list. 274 * 275 * @param resultType the type to marshal each row to 276 * @param <T> the result type 277 * @return list of results (empty if no rows) 278 */ 279 @NonNull 280 <T> List<@Nullable T> fetchList(@NonNull Class<T> resultType); 281 282 /** 283 * Executes the query and provides a {@link Stream} backed by the underlying {@link java.sql.ResultSet}. 284 * <p> 285 * 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. 286 * <p> 287 * JDBC resources are closed automatically when {@code streamFunction} returns (or throws), so the stream must be fully consumed 288 * within that callback. Do not escape the stream from the function. 289 * <p> 290 * The stream must be consumed within the scope of the transaction or connection that created it. 291 * If the stream participates in a Pyranid transaction, it must also be closed by the thread that opened it. 292 * <p> 293 * Supported dialects apply driver-specific streaming setup automatically. PostgreSQL streams use a positive JDBC 294 * fetch size and an autocommit-disabled connection when no Pyranid transaction is active; MySQL streams use 295 * forward-only/read-only statements and the MySQL streaming fetch-size sentinel; MariaDB streams use 296 * forward-only/read-only statements without the MySQL sentinel. Use {@link #fetchSize(Integer)} to override 297 * fetch-size behavior when needed. 298 * 299 * @param resultType the type to marshal each row to 300 * @param streamFunction function that consumes the stream and returns a result 301 * @param <T> the result type 302 * @param <R> the return type 303 * @return the value returned by {@code streamFunction} 304 */ 305 @Nullable 306 <T, R> R fetchStream(@NonNull Class<T> resultType, 307 @NonNull Function<Stream<@Nullable T>, R> streamFunction); 308 309 310 /** 311 * Executes a DML statement (INSERT, UPDATE, DELETE) with no resultset. 312 * 313 * @return the number of rows affected 314 */ 315 @NonNull 316 Long execute(); 317 318 /** 319 * Executes a DML statement and maps a single JDBC-generated key row. 320 * <p> 321 * This uses JDBC {@link java.sql.Statement#RETURN_GENERATED_KEYS}. It is intended for database-generated values such 322 * as identity/auto-increment primary keys. If your SQL returns rows directly via database syntax such as PostgreSQL 323 * {@code RETURNING} or SQL Server {@code OUTPUT}, use {@link #executeForObject(Class)} instead. 324 * <p> 325 * For SQL Server multi-row identity inserts, use {@code OUTPUT} with {@link #executeForList(Class)} instead of JDBC 326 * generated keys. For MySQL {@code INSERT ... ON DUPLICATE KEY UPDATE}, use the {@code LAST_INSERT_ID(id)} idiom if 327 * you need the existing row's ID returned on the update path. 328 * <p> 329 * Oracle requires explicit generated-key column names; use {@link #executeReturningGeneratedKey(Class, String...)} 330 * instead. 331 * 332 * @param resultType the type to marshal the generated-key row to 333 * @param <T> the result type 334 * @return the single generated key row, or empty if the driver returns no generated keys 335 * @throws DatabaseException if more than one generated-key row is returned 336 * @since 4.2.0 337 */ 338 @NonNull 339 default <T> Optional<T> executeReturningGeneratedKey(@NonNull Class<T> resultType) { 340 throw new UnsupportedOperationException("executeReturningGeneratedKey is not supported by this Query implementation"); 341 } 342 343 /** 344 * Executes a DML statement and maps a single JDBC-generated key row. 345 * <p> 346 * This uses JDBC {@link java.sql.Connection#prepareStatement(String, String[])} with the supplied key column names. 347 * Some drivers require column names to return generated keys for specific columns, especially when more than one 348 * generated value is available. If {@code keyColumnNames} is empty, this behaves like 349 * {@link #executeReturningGeneratedKey(Class)}, except for dialects such as Oracle which require explicit names. 350 * <p> 351 * For SQL Server multi-row identity inserts, use {@code OUTPUT} with {@link #executeForList(Class)} instead of JDBC 352 * generated keys. For MySQL {@code INSERT ... ON DUPLICATE KEY UPDATE}, use the {@code LAST_INSERT_ID(id)} idiom if 353 * you need the existing row's ID returned on the update path. 354 * 355 * @param resultType the type to marshal the generated-key row to 356 * @param keyColumnNames generated-key column names requested from the driver 357 * @param <T> the result type 358 * @return the single generated key row, or empty if the driver returns no generated keys 359 * @throws DatabaseException if more than one generated-key row is returned 360 * @since 4.2.0 361 */ 362 @NonNull 363 default <T> Optional<T> executeReturningGeneratedKey(@NonNull Class<T> resultType, 364 @NonNull String @NonNull ... keyColumnNames) { 365 throw new UnsupportedOperationException("executeReturningGeneratedKey is not supported by this Query implementation"); 366 } 367 368 /** 369 * Executes a DML statement and maps all JDBC-generated key rows. 370 * <p> 371 * This uses JDBC {@link java.sql.Statement#RETURN_GENERATED_KEYS}. It is intended for database-generated values such 372 * as identity/auto-increment primary keys. If your SQL returns rows directly via database syntax such as PostgreSQL 373 * {@code RETURNING} or SQL Server {@code OUTPUT}, use {@link #executeForList(Class)} instead. 374 * <p> 375 * For SQL Server multi-row identity inserts, use {@code OUTPUT} with {@link #executeForList(Class)} instead of JDBC 376 * generated keys. For MySQL {@code INSERT ... ON DUPLICATE KEY UPDATE}, use the {@code LAST_INSERT_ID(id)} idiom if 377 * you need the existing row's ID returned on the update path. 378 * <p> 379 * Oracle requires explicit generated-key column names; use {@link #executeReturningGeneratedKeys(Class, String...)} 380 * instead. 381 * 382 * @param resultType the type to marshal each generated-key row to 383 * @param <T> the result type 384 * @return list of generated key rows 385 * @since 4.2.0 386 */ 387 @NonNull 388 default <T> List<@Nullable T> executeReturningGeneratedKeys(@NonNull Class<T> resultType) { 389 throw new UnsupportedOperationException("executeReturningGeneratedKeys is not supported by this Query implementation"); 390 } 391 392 /** 393 * Executes a DML statement and maps all JDBC-generated key rows. 394 * <p> 395 * This uses JDBC {@link java.sql.Connection#prepareStatement(String, String[])} with the supplied key column names. 396 * Some drivers require column names to return generated keys for specific columns, especially when more than one 397 * generated value is available. If {@code keyColumnNames} is empty, this behaves like 398 * {@link #executeReturningGeneratedKeys(Class)}, except for dialects such as Oracle which require explicit names. 399 * <p> 400 * For SQL Server multi-row identity inserts, use {@code OUTPUT} with {@link #executeForList(Class)} instead of JDBC 401 * generated keys. For MySQL {@code INSERT ... ON DUPLICATE KEY UPDATE}, use the {@code LAST_INSERT_ID(id)} idiom if 402 * you need the existing row's ID returned on the update path. 403 * 404 * @param resultType the type to marshal each generated-key row to 405 * @param keyColumnNames generated-key column names requested from the driver 406 * @param <T> the result type 407 * @return list of generated key rows 408 * @since 4.2.0 409 */ 410 @NonNull 411 default <T> List<@Nullable T> executeReturningGeneratedKeys(@NonNull Class<T> resultType, 412 @NonNull String @NonNull ... keyColumnNames) { 413 throw new UnsupportedOperationException("executeReturningGeneratedKeys is not supported by this Query implementation"); 414 } 415 416 /** 417 * Executes a DML statement in batch over groups of named parameters. 418 * <p> 419 * Any parameters already bound on this {@code Query} apply to all groups; group values override them. 420 * Each group must provide a complete set of parameter values after merging; groups must be non-null and 421 * expand to the same number of JDBC parameters (for example, IN-list sizes must match). 422 * 423 * @param parameterGroups groups of named parameter values (without the leading {@code :}) 424 * @return the number of rows affected by the SQL statement per-group 425 */ 426 @NonNull 427 List<Long> executeBatch(@NonNull List<@NonNull Map<@NonNull String, @Nullable Object>> parameterGroups); 428 429 /** 430 * Executes a DML statement that returns a single row (for example, with PostgreSQL/SQLite {@code RETURNING}, 431 * MariaDB {@code INSERT ... RETURNING}, or SQL Server {@code OUTPUT}). 432 * 433 * @param resultType the type to marshal the row to 434 * @param <T> the result type 435 * @return the single result, or empty if no rows 436 * @throws DatabaseException if more than one row is returned 437 */ 438 @NonNull 439 <T> Optional<T> executeForObject(@NonNull Class<T> resultType); 440 441 /** 442 * Executes a DML statement that returns multiple rows (for example, with PostgreSQL/SQLite {@code RETURNING}, 443 * MariaDB {@code INSERT ... RETURNING}, or SQL Server {@code OUTPUT}). 444 * 445 * @param resultType the type to marshal each row to 446 * @param <T> the result type 447 * @return list of results 448 */ 449 @NonNull 450 <T> List<@Nullable T> executeForList(@NonNull Class<T> resultType); 451}