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}