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}