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}