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}