All Downloads are FREE. Search and download functionalities are using the official Maven repository.

objectos.way.Sql Maven / Gradle / Ivy

Go to download

Objectos Way allows you to build full-stack web applications using only Java.

The newest version!
/*
 * Copyright (C) 2023-2024 Objectos Software LTDA.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package objectos.way;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.RecordComponent;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.OptionalInt;
import java.util.function.Consumer;
import javax.sql.DataSource;

/**
 * The Objectos SQL main class.
 */
public final class Sql {

  // trx isolation levels

  public static final Transaction.Isolation READ_UNCOMMITED = TransactionIsolation.READ_UNCOMMITED;

  public static final Transaction.Isolation READ_COMMITED = TransactionIsolation.READ_COMMITED;

  public static final Transaction.Isolation REPEATABLE_READ = TransactionIsolation.REPEATABLE_READ;

  public static final Transaction.Isolation SERIALIZABLE = TransactionIsolation.SERIALIZABLE;

  // types

  /**
   * A source for SQL database connections. It typically wraps a
   * {@link DataSource} instance.
   */
  public sealed interface Database permits SqlDatabase {

    /**
     * Configures the creation of a {@code Sql.Database} instance.
     */
    public sealed interface Config permits SqlDatabaseConfig {

      void dataSource(DataSource value);

      /**
       * Sets the note sink to the specified value.
       *
       * @param value
       *        a note sink instance
       */
      void noteSink(Note.Sink value);

    }

    /**
     * Creates a new {@code Sql.Database} instance with the specified
     * configuration.
     *
     * @param config
     *        configuration options of this new instance
     *
     * @return a newly created database instance
     *
     * @throws DatabaseException
     *         if a database access error occurs
     */
    static Database create(Consumer config) throws DatabaseException {
      SqlDatabaseConfig builder;
      builder = new SqlDatabaseConfig();

      config.accept(builder);

      return builder.build();
    }

    /**
     * Begins a transaction with the specified isolation level.
     *
     * @param level
     *        the transaction isolation level
     *
     * @return a connection to the underlying database with a transaction
     *         started with the specified isolation level
     *
     * @throws DatabaseException
     *         if a database access error occurs
     */
    Transaction beginTransaction(Transaction.Isolation level) throws DatabaseException;

  }

  /**
   * Thrown to indicate a database access error.
   */
  public static class DatabaseException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    DatabaseException(SQLException cause) {
      super(cause);
    }

    @Override
    public final SQLException getCause() {
      return (SQLException) super.getCause();
    }

  }

  public sealed interface GeneratedKeys {

    public sealed interface OfInt extends GeneratedKeys {

      int getAsInt(int index);

    }

    T get(int index);

    int size();

  }

  /**
   * Maps a row from a {@code ResultSet} to an object of type {@code T}.
   */
  @FunctionalInterface
  public interface Mapper {

    /**
     * Implementations must not invoke the {@code next()} method on the
     * {@code ResultSet} object and they must not return {@code null} values.
     *
     * @param rs
     *        the result set object positioned at the row to be mapped
     * @param startingParameterIndex
     *        the starting parameter index (always the value {@code 1})
     *
     * @return the mapped object, never {@code null}
     */
    T map(ResultSet rs, int startingParameterIndex) throws SQLException;

  }

  public interface Page {

    /**
     * The page number. Page numbers are always greater than zero.
     * In other words, the first page is page number 1.
     * The second page is page number 2 and so on.
     *
     * @return the page number
     */
    int number();

    /**
     * The number of items to be displayed on each page.
     *
     * @return the number of items to be displayed on each page
     */
    int size();

  }

  /**
   * A provider of {@link Sql.Page} instances.
   */
  @FunctionalInterface
  public interface PageProvider {

    /**
     * The current page.
     *
     * @return the current page
     */
    Page page();

  }

  /**
   * Responsible for processing the results of a query operation.
   */
  @FunctionalInterface
  public interface QueryProcessor {

    /**
     * Process the entire result set.
     *
     * @param rs
     *        the result set to be processed
     *
     * @throws SQLException
     *         if the result set throws
     */
    void process(ResultSet rs) throws SQLException;

  }

  /**
   * A connection to a running transaction in a database.
   */
  public sealed interface Transaction permits SqlTransaction {

    /**
     * The isolation level of a transaction.
     */
    public sealed interface Isolation {}

    /**
     * Commits this transaction.
     *
     * @throws DatabaseException
     *         if the underlying {@link Connection#commit()} method throws
     */
    void commit() throws DatabaseException;

    /**
     * Undoes all changes made in this transaction.
     *
     * @throws DatabaseException
     *         if the underlying {@link Connection#rollback()} method throws
     */
    void rollback() throws DatabaseException;

    /**
     * Rolls back this transaction and returns the specified exception. If the
     * rollback operation throws then the thrown exception is added as a
     * suppressed exception to the specified exception.
     *
     * 

* A typical usage is: * *

     * Sql.Transaction sql = source.beginTransaction(Sql.SERIALIZABLE);
     *
     * try {
     *   // code that may throw
     * } catch (KnownException e) {
     *   throw trx.rollbackAndSuppress(e);
     * } catch (Throwable t) {
     *   logger.log("Operation failed", trx.rollbackAndSuppress(t));
     * } finally {
     *   trx.close();
     * }
* * @param error * a throwable instance * @param * the type of the throwable instance * * @return the specified throwable which may or may not contain a new * suppressed exception (from the rollback operation) */ T rollbackAndSuppress(T error); /** * Rolls back this transaction, wraps the specified throwable into an * unchecked exception and returns the wrapping exception. If the rollback * operation throws then the thrown exception is added as a suppressed * exception to the wrapping exception. * *

* A typical usage is: * *

     * Sql.Transaction sql = source.beginTransaction(Sql.SERIALIZABLE);
     *
     * try {
     *   // code that may throw
     * } catch (Throwable t) {
     *   throw trx.rollbackAndWrap(t);
     * } finally {
     *   trx.close();
     * }
* * @param error * the throwable to be wrapped * * @return a newly created wrapping exception whose cause is the specified * throwable */ RollbackWrapperException rollbackAndWrap(Throwable error); /** * Closes the underlying database connection. */ void close() throws DatabaseException; int count(String sql, Object... args) throws DatabaseException; /** * Use the specified processor to process the results of the execution of * the specified row-retriving SQL statement. The specified arguments * are applied, in order, to the resulting prepared statement prior to * sending the query to the database. * * @param processor * the processor to process the results * @param sql * the row-retriving SQL statement to be executed * @param args * the arguments of the SQL statement * * @throws DatabaseException * if a database access error occurs */ void processQuery(QueryProcessor processor, String sql, Object... args) throws DatabaseException; /** * Use the specified processor to process the results of the execution of * a row-retriving SQL statement that is obtained by applying the specified * page to the specified SQL statement. The specified arguments * are applied, in order, to the resulting prepared statement prior to * sending the query to the database. * * @param processor * the processor to process the results * @param page * limit the processing to this page * @param sql * the row-retriving SQL statement to which the page will be applied * @param args * the arguments of the SQL statement * * @throws DatabaseException * if a database access error occurs */ void processQuery(QueryProcessor processor, Page page, String sql, Object... args) throws DatabaseException; default void processQuery(QueryProcessor processor, PageProvider pageProvider, String sql, Object... args) throws DatabaseException { Page page; page = pageProvider.page(); // implicit null-check processQuery(processor, page, sql, args); } /** * Sets the SQL contents of this transaction to the specified value. * *

* Invoking this method additionally: * *

    *
  • clears any previously set arguments;
  • *
* * @param value * the raw SQL contents * * @return this object */ Transaction sql(String value); /** * Replaces the current SQL statement with the result of * applying the specified arguments to the SQL statement as if it were a * format string. * * @param args * arguments referenced by the format specifiers in the format * string. * * @return this object */ Transaction format(Object... args); /** * Adds the specified value to the SQL statement argument list. * * @param value * the argument value which must not be {@code null} * * @return this object */ Transaction add(Object value); /** * Adds the specified value to the SQL statement argument list. * * @param value * the argument value which may be {@code null} * @param sqlType * the SQL type (as defined in java.sql.Types) * * @return this object */ Transaction add(Object value, int sqlType); Transaction addBatch(); int[] batchUpdate(); /** * Executes the current SQL statement as a row-retrieving query. */ List query(Mapper mapper) throws DatabaseException; /** * Executes the current SQL statement as a row-retrieving query which must * return a single result (no more and no less). */ T querySingle(Mapper mapper) throws DatabaseException; /** * Executes the current SQL statement as a row-retrieving query which may * return a single result or {@code null} if there were no results. */ T queryNullable(Mapper mapper) throws DatabaseException; OptionalInt queryOptionalInt() throws DatabaseException; /** * Executes the current SQL contents as a script. The SQL contents is * assumed to be a blank line separated list of SQL statements. The * statements will typically be SQL {@code INSERT} or {@code UPDATE} * statements. * *

* A typical usage is: * *

     * trx.sql("""
     * insert into City (id, name)
     * values (1, 'São Paulo')
     * ,      (2, 'New York')
     * ,      (3, 'Tokyo')
     *
     * insert into Country (id, name)
     * values (1, 'Brazil')
     * ,      (2, 'United States of America')
     * ,      (3, 'Japan')
     * """);
     *
     * int[] result = trx.scriptUpdate();
     *
     * assertEquals(result.length, 2);
     * assertEquals(result[0], 3);
     * assertEquals(result[1], 3);
* * @return an array of update counts containing one element for each * statement in the script. The elements of the array are ordered * according to the order in which statements were listed in the * script. */ int[] scriptUpdate() throws DatabaseException; /** * Executes the current SQL statement as an update operation. */ int update() throws DatabaseException; /** * Executes the current SQL statement as an update operation. */ int updateWithGeneratedKeys(GeneratedKeys generatedKeys) throws DatabaseException; } static final class MappingException extends RuntimeException { private static final long serialVersionUID = -3104952657116253825L; MappingException(String message) { super(message); } MappingException(String message, Throwable cause) { super(message, cause); } } public static final class RollbackWrapperException extends RuntimeException { private static final long serialVersionUID = 6575236786994565106L; RollbackWrapperException(Throwable cause) { super(cause); } } private Sql() {} public static GeneratedKeys.OfInt createGeneratedKeysOfInt() { return new SqlGeneratedKeysOfInt(); } public static Page createPage(int number, int size) { Check.argument(number > 0, "number must be positive"); Check.argument(size > 0, "size must be positive"); record SqlPage(int number, int size) implements Sql.Page {} return new SqlPage(number, size); } /** * Creates a {@link Sql.Mapper} for the specified record type. */ public static Mapper createRecordMapper(Class recordType) { return RecordMapper.of(recordType); } /** * Undoes all changes made in the specified transaction and closes its * underlying database connection. * * @param trx * the transaction object * * @throws DatabaseException * if a database access error occurs */ public static void rollbackAndClose(Sql.Transaction trx) throws DatabaseException { Objects.requireNonNull(trx, "trx == null"); SqlTransaction impl; impl = (SqlTransaction) trx; impl.rollbackAndClose(); } // utils private record Null(int sqlType) {} static Object nullable(Object value, int sqlType) { if (value == null) { return new Null(sqlType); } else { return value; } } static void set(PreparedStatement stmt, int index, Object value) throws SQLException { switch (value) { case Boolean b -> stmt.setBoolean(index, b.booleanValue()); case Double d -> stmt.setDouble(index, d.doubleValue()); case Float f -> stmt.setFloat(index, f.floatValue()); case Integer i -> stmt.setInt(index, i.intValue()); case LocalDate ld -> stmt.setObject(index, ld); case LocalDateTime dt -> stmt.setObject(index, dt); case Long i -> stmt.setLong(index, i.longValue()); case String s -> stmt.setString(index, s); case Null x -> stmt.setNull(index, x.sqlType); default -> throw new IllegalArgumentException("Unexpected type: " + value.getClass()); } } // non-public types static non-sealed abstract class SqlGeneratedKeys implements Sql.GeneratedKeys { final void accept(Statement stmt) throws SQLException { try (ResultSet generatedKeys = stmt.getGeneratedKeys()) { clear(); accept(generatedKeys); } } abstract void accept(ResultSet rs) throws SQLException; abstract void clear(); } static final class SqlGeneratedKeysOfInt extends SqlGeneratedKeys implements Sql.GeneratedKeys.OfInt { private int[] keys = Util.EMPTY_INT_ARRAY; private int size = 0; @Override public final Integer get(int index) { return getAsInt(index); } @Override public final int getAsInt(int index) { Objects.checkIndex(index, size); return keys[index]; } @Override public final int size() { return size; } @Override final void accept(ResultSet rs) throws SQLException { while (rs.next()) { int value; value = rs.getInt(1); add(value); } } @Override final void clear() { size = 0; } private void add(int value) { int requiredIndex; requiredIndex = size++; keys = Util.growIfNecessary(keys, requiredIndex); keys[requiredIndex] = value; } } private static final class RecordMapper implements Mapper { private final Class[] types; private final Constructor constructor; private final Object[] values; RecordMapper(Class[] types, Constructor constructor) { this.types = types; this.constructor = constructor; this.values = new Object[types.length]; } static RecordMapper of(Class recordType) { RecordComponent[] components; // early implicit null-check components = recordType.getRecordComponents(); Class[] types; types = new Class[components.length]; for (int idx = 0; idx < components.length; idx++) { RecordComponent component; component = components[idx]; types[idx] = component.getType(); } Constructor constructor; try { constructor = recordType.getDeclaredConstructor(types); if (!constructor.canAccess(null)) { constructor.setAccessible(true); } } catch (NoSuchMethodException | SecurityException e) { throw new Sql.MappingException("Failed to obtain record canonical constructor", e); } return new RecordMapper(types, constructor); } @Override public final R map(ResultSet rs, int startingIndex) throws SQLException { try { for (int idx = 0, len = types.length; idx < len; idx++) { Class type; type = types[idx]; Object value; value = rs.getObject(idx + 1, type); values[idx] = value; } return constructor.newInstance(values); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new Sql.MappingException("Failed to create record instance", e); } } @SuppressWarnings("unused") final void checkColumnCount(ResultSet rs) throws SQLException { ResultSetMetaData meta; meta = rs.getMetaData(); int columnCount; columnCount = meta.getColumnCount(); if (columnCount != types.length) { throw new Sql.MappingException("Query returned " + columnCount + " columns but record has only " + types.length + " components"); } } } enum TransactionIsolation implements Transaction.Isolation { READ_UNCOMMITED(Connection.TRANSACTION_READ_UNCOMMITTED), READ_COMMITED(Connection.TRANSACTION_READ_COMMITTED), REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ), SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE); final int jdbcValue; private TransactionIsolation(int level) { this.jdbcValue = level; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy