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}