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 javax.annotation.concurrent.ThreadSafe;
024import java.time.ZoneId;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collections;
028import java.util.List;
029import java.util.Objects;
030import java.util.Optional;
031import java.util.Queue;
032import java.util.concurrent.ConcurrentLinkedQueue;
033import java.util.function.Supplier;
034import java.util.stream.Collectors;
035
036import static java.lang.String.format;
037import static java.util.Objects.requireNonNull;
038
039/**
040 * Data that represents a SQL statement.
041 *
042 * @author <a href="https://www.revetkn.com">Mark Allen</a>
043 * @since 2.0.0
044 */
045@ThreadSafe
046public final class StatementContext<T> {
047        @NonNull
048        private final Statement statement;
049        @NonNull
050        private final List<@Nullable Object> parameters;
051        @Nullable
052        private final Class<T> resultSetRowType;
053        @NonNull
054        private final Supplier<@NonNull DatabaseType> databaseTypeSupplier;
055        @NonNull
056        private final Supplier<@NonNull DatabaseDialect> databaseDialectSupplier;
057        @Nullable
058        private volatile DatabaseDialect databaseDialect;
059        @NonNull
060        private final Supplier<@NonNull DatabaseType> diagnosticDatabaseTypeSupplier;
061        @NonNull
062        private final ZoneId timeZone;
063        @NonNull
064        private final AmbiguousTimestampBindingStrategy ambiguousTimestampBindingStrategy;
065        @NonNull
066        private final ParameterRedactor parameterRedactor;
067        private final boolean batchParameterGroups;
068        @Nullable
069        private final SpiOverrides spiOverrides;
070        @NonNull
071        private final Queue<@NonNull AutoCloseable> cleanupOperations;
072
073        /**
074         * Per-query overrides of the {@link Database}-wide mapping/binding SPIs, carried on the context so
075         * execution internals can resolve them without additional plumbing. Deliberately excluded from
076         * {@link #equals(Object)}/{@link #hashCode()}/{@link #toString()} - overrides are functional
077         * identity, not value identity.
078         */
079        record SpiOverrides(@Nullable ResultSetMapper resultSetMapper,
080                                                                                        @Nullable PreparedStatementBinder preparedStatementBinder) {}
081
082        StatementContext(@NonNull Builder builder) {
083                requireNonNull(builder);
084
085                this.statement = builder.statement;
086                this.parameters = builder.parameters == null
087                                ? List.of()
088                                : Collections.unmodifiableList(new ArrayList<>(builder.parameters));
089                this.resultSetRowType = builder.resultSetRowType;
090                this.databaseTypeSupplier = builder.databaseTypeSupplier;
091                this.databaseDialectSupplier = builder.databaseDialectSupplier;
092                this.diagnosticDatabaseTypeSupplier = builder.diagnosticDatabaseTypeSupplier;
093                this.timeZone = builder.timeZone;
094                this.ambiguousTimestampBindingStrategy = builder.ambiguousTimestampBindingStrategy;
095                this.parameterRedactor = builder.parameterRedactor;
096                this.batchParameterGroups = builder.batchParameterGroups;
097                this.spiOverrides = builder.spiOverrides;
098                this.cleanupOperations = new ConcurrentLinkedQueue<>();
099        }
100
101        @Nullable
102        ResultSetMapper getResultSetMapperOverride() {
103                return this.spiOverrides == null ? null : this.spiOverrides.resultSetMapper();
104        }
105
106        @Nullable
107        PreparedStatementBinder getPreparedStatementBinderOverride() {
108                return this.spiOverrides == null ? null : this.spiOverrides.preparedStatementBinder();
109        }
110
111        @Override
112        public int hashCode() {
113                return Objects.hash(getStatement(), getParameters(), getResultSetRowType(), getDiagnosticDatabaseType(), getTimeZone(), getAmbiguousTimestampBindingStrategy());
114        }
115
116        @Override
117        public boolean equals(Object object) {
118                if (this == object)
119                        return true;
120
121                if (!(object instanceof StatementContext))
122                        return false;
123
124                StatementContext statementContext = (StatementContext) object;
125
126                return Objects.equals(statementContext.getStatement(), getStatement())
127                                && Objects.equals(statementContext.getParameters(), getParameters())
128                                && Objects.equals(statementContext.getResultSetRowType(), getResultSetRowType())
129                                && Objects.equals(statementContext.getDiagnosticDatabaseType(), getDiagnosticDatabaseType())
130                                && Objects.equals(statementContext.getTimeZone(), getTimeZone())
131                                && Objects.equals(statementContext.getAmbiguousTimestampBindingStrategy(), getAmbiguousTimestampBindingStrategy());
132        }
133
134        @Override
135        public String toString() {
136                List<String> components = new ArrayList<>(3);
137
138                components.add(format("statement=%s", getStatement()));
139
140                if (getParameters().size() > 0)
141                        components.add(format("parameters=%s", getRedactedParameters()));
142
143                Class<T> resultSetRowType = getResultSetRowType().orElse(null);
144
145                if (resultSetRowType != null)
146                        components.add(format("resultSetRowType=%s", resultSetRowType));
147
148                components.add(format("databaseType=%s", getDiagnosticDatabaseType().name()));
149                components.add(format("timeZone=%s", getTimeZone().getId()));
150                components.add(format("ambiguousTimestampBindingStrategy=%s", getAmbiguousTimestampBindingStrategy().name()));
151
152                return format("%s{%s}", getClass().getSimpleName(), components.stream().collect(Collectors.joining(", ")));
153        }
154
155        @NonNull
156        public Statement getStatement() {
157                return this.statement;
158        }
159
160        @NonNull
161        public List<@Nullable Object> getParameters() {
162                return this.parameters;
163        }
164
165        /**
166         * Gets this statement's parameters rendered for diagnostics.
167         * <p>
168         * {@link SecureParameter} values render as their masks. Other non-batch values are rendered through the configured
169         * {@link ParameterRedactor}. Batch executions render a bounded summary instead of individual group values.
170         *
171         * @return parameters rendered for diagnostics
172         * @since 4.4.0
173         */
174        @NonNull
175        public List<@Nullable Object> getRedactedParameters() {
176                if (this.batchParameterGroups)
177                        return List.of(batchParameterSummary());
178
179                List<@Nullable Object> redactedParameters = new ArrayList<>(getParameters().size());
180
181                for (int i = 0; i < getParameters().size(); ++i) {
182                        Object parameter = getParameters().get(i);
183                        SecureParameter secureParameter = SecureParameterSupport.displaySecureParameter(parameter);
184
185                        if (secureParameter != null)
186                                redactedParameters.add(SecureParameterSupport.maskOf(secureParameter));
187                        else
188                                redactedParameters.add(this.parameterRedactor.redactParameter(this, i, parameter));
189                }
190
191                return Collections.unmodifiableList(redactedParameters);
192        }
193
194        @NonNull
195        private String batchParameterSummary() {
196                List<@Nullable Object> parameters = getParameters();
197                int groupCount = parameters.size();
198
199                if (groupCount == 0)
200                        return "<batch: 0 groups x 0 parameters>";
201
202                Integer expectedParameterCount = null;
203                boolean mixedParameterCounts = false;
204
205                for (Object parameterGroup : parameters) {
206                        int parameterCount = parameterGroup instanceof List<?> list ? list.size() : 0;
207
208                        if (expectedParameterCount == null) {
209                                expectedParameterCount = parameterCount;
210                        } else if (expectedParameterCount != parameterCount) {
211                                mixedParameterCounts = true;
212                                break;
213                        }
214                }
215
216                if (mixedParameterCounts)
217                        return format("<batch: %s groups x mixed parameter counts>", groupCount);
218
219                return format("<batch: %s groups x %s parameters>", groupCount, expectedParameterCount);
220        }
221
222        @NonNull
223        public Optional<Class<T>> getResultSetRowType() {
224                return Optional.ofNullable(this.resultSetRowType);
225        }
226
227        /**
228         * Gets the database type for this statement.
229         * <p>
230         * If automatic database type detection is enabled and the type has not already been detected, this method may acquire a
231         * connection and inspect {@link java.sql.DatabaseMetaData}. Diagnostic methods such as {@link #toString()},
232         * {@link #equals(Object)}, and {@link #hashCode()} use a non-detecting database-type value instead.
233         *
234         * @return the database type
235         * @throws DatabaseException if automatic database type detection fails
236         * @since 3.0.0
237         */
238        @NonNull
239        public DatabaseType getDatabaseType() {
240                return this.databaseTypeSupplier.get();
241        }
242
243        @NonNull
244        DatabaseDialect getDatabaseDialect() {
245                DatabaseDialect cachedDatabaseDialect = this.databaseDialect;
246
247                if (cachedDatabaseDialect != null)
248                        return cachedDatabaseDialect;
249
250                DatabaseDialect databaseDialect = this.databaseDialectSupplier.get();
251                this.databaseDialect = databaseDialect;
252                return databaseDialect;
253        }
254
255        @NonNull
256        private DatabaseType getDiagnosticDatabaseType() {
257                return this.diagnosticDatabaseTypeSupplier.get();
258        }
259
260        @NonNull
261        public ZoneId getTimeZone() {
262                return this.timeZone;
263        }
264
265        /**
266         * How should Pyranid bind {@link java.time.Instant} and {@link java.time.OffsetDateTime} parameters when JDBC
267         * parameter metadata cannot identify whether the target is {@code TIMESTAMP} or {@code TIMESTAMP WITH TIME ZONE}?
268         *
269         * @return behavior to use when timestamp target metadata is unavailable or non-identifying
270         * @since 4.2.0
271         */
272        @NonNull
273        public AmbiguousTimestampBindingStrategy getAmbiguousTimestampBindingStrategy() {
274                return this.ambiguousTimestampBindingStrategy;
275        }
276
277        void addCleanupOperation(@NonNull AutoCloseable cleanupOperation) {
278                requireNonNull(cleanupOperation);
279                this.cleanupOperations.add(cleanupOperation);
280        }
281
282        @NonNull
283        Queue<@NonNull AutoCloseable> getCleanupOperations() {
284                return this.cleanupOperations;
285        }
286
287        @NonNull
288        public static <T> Builder<T> with(@NonNull Statement statement,
289                                                                                                                                                @NonNull Database database) {
290                requireNonNull(statement);
291                requireNonNull(database);
292
293                return new Builder<>(statement, database);
294        }
295
296        /**
297         * Builder used to construct instances of {@link StatementContext}.
298         * <p>
299         * This class is intended for use by a single thread.
300         *
301         * @author <a href="https://www.revetkn.com">Mark Allen</a>
302         * @since 2.0.0
303         */
304        @NotThreadSafe
305        public static class Builder<T> {
306                @NonNull
307                private final Statement statement;
308                @NonNull
309                private final Supplier<@NonNull DatabaseType> databaseTypeSupplier;
310                @NonNull
311                private final Supplier<@NonNull DatabaseDialect> databaseDialectSupplier;
312                @NonNull
313                private final Supplier<@NonNull DatabaseType> diagnosticDatabaseTypeSupplier;
314                @NonNull
315                private final ZoneId timeZone;
316                @NonNull
317                private final AmbiguousTimestampBindingStrategy ambiguousTimestampBindingStrategy;
318                @NonNull
319                private final ParameterRedactor parameterRedactor;
320                @Nullable
321                private List<@Nullable Object> parameters;
322                @Nullable
323                private Class<T> resultSetRowType;
324                private boolean batchParameterGroups;
325                @Nullable
326                private SpiOverrides spiOverrides;
327
328                private Builder(@NonNull Statement statement,
329                                                                                @NonNull Database database) {
330                        requireNonNull(statement);
331                        requireNonNull(database);
332
333                        this.statement = statement;
334                        this.databaseTypeSupplier = database::getDatabaseType;
335                        this.databaseDialectSupplier = database::getDatabaseDialect;
336                        this.diagnosticDatabaseTypeSupplier = database::peekDatabaseType;
337                        this.timeZone = database.getTimeZone();
338                        this.ambiguousTimestampBindingStrategy = database.getAmbiguousTimestampBindingStrategy();
339                        this.parameterRedactor = database.getParameterRedactor();
340                }
341
342                @NonNull
343                public Builder parameters(@Nullable List<@Nullable Object> parameters) {
344                        this.parameters = parameters;
345                        return this;
346                }
347
348                @NonNull
349                public Builder parameters(Object @Nullable ... parameters) {
350                        this.parameters = parameters == null ? null : Arrays.asList(parameters);
351                        return this;
352                }
353
354                @NonNull
355                public Builder resultSetRowType(Class<T> resultSetRowType) {
356                        this.resultSetRowType = resultSetRowType;
357                        return this;
358                }
359
360                @NonNull
361                Builder batchParameterGroups(boolean batchParameterGroups) {
362                        this.batchParameterGroups = batchParameterGroups;
363                        return this;
364                }
365
366                @NonNull
367                Builder spiOverrides(@Nullable SpiOverrides spiOverrides) {
368                        this.spiOverrides = spiOverrides;
369                        return this;
370                }
371
372                @NonNull
373                public StatementContext build() {
374                        return new StatementContext<>(this);
375                }
376        }
377}