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