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

io.cloudstate.proxy.jdbc.SlickCreateTables.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2019 Lightbend Inc.
 *
 * 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 io.cloudstate.proxy.jdbc

import java.sql.Connection

import akka.Done
import akka.actor.ActorSystem
import akka.event.Logging
import akka.persistence.jdbc.config.{JournalTableConfiguration, SnapshotTableConfiguration}
import akka.persistence.jdbc.journal.dao.JournalTables
import akka.persistence.jdbc.snapshot.dao.SnapshotTables
import akka.persistence.jdbc.util.SlickDatabase
import io.cloudstate.proxy.jdbc.SlickCreateTables.TableConfiguration
import io.cloudstate.proxy.valueentity.store.jdbc.{JdbcEntityTable, JdbcEntityTableConfiguration, JdbcSlickDatabase}
import slick.jdbc.{H2Profile, JdbcBackend, JdbcProfile, MySQLProfile, PostgresProfile}
import slick.jdbc.meta.MTable

import scala.concurrent.Future
import scala.util.Failure
import scala.util.Success
import scala.util.Try

object SlickCreateTables {

  case class TableConfiguration(schemaStatements: Seq[String], schemaName: Option[String], tableName: String)

}

trait SlickCreateTables {
  val system: ActorSystem
  val profile: JdbcProfile
  val database: JdbcBackend.Database
  def tableConfigurations: Seq[TableConfiguration]

  import profile.api._
  import system.dispatcher

  private final val log = Logging(system.eventStream, classOf[SlickCreateTables])

  def run(): Future[Done] =
    database.run {
      for {
        _ <- slick.dbio.DBIO.sequence(
          tableConfigurations.map(r => createTable(r.schemaStatements, tableExists(r.schemaName, r.tableName)))
        )
      } yield Done.getInstance()
    }

  private def createTable(schemaStatements: Seq[String], tableExists: (Vector[MTable], Option[String]) => Boolean) =
    for {
      currentSchema <- getCurrentSchema
      tables <- getTables(currentSchema)
      _ <- createTableInternal(tables, currentSchema, schemaStatements, tableExists)
    } yield Done.getInstance()

  private def createTableInternal(
      tables: Vector[MTable],
      currentSchema: Option[String],
      schemaStatements: Seq[String],
      tableExists: (Vector[MTable], Option[String]) => Boolean
  ) =
    if (tableExists(tables, currentSchema)) {
      DBIO.successful(())
    } else {
      if (log.isDebugEnabled) {
        log.debug("Creating table, executing: " + schemaStatements.mkString("; "))
      }

      DBIO
        .sequence(schemaStatements.map { s =>
          SimpleDBIO { ctx =>
            val stmt = ctx.connection.createStatement()
            try {
              stmt.executeUpdate(s)
            } finally {
              stmt.close()
            }
          }
        })
        .asTry
        .flatMap {
          case Success(_) => DBIO.successful(())
          case Failure(f) =>
            getTables(currentSchema).map { tables =>
              if (tableExists(tables, currentSchema)) {
                log.debug("Table creation failed, but table existed after it was created, ignoring failure", f)
                ()
              } else {
                throw f
              }
            }
        }
    }

  private def getTables(currentSchema: Option[String]) =
    // Calling MTable.getTables without parameters fails on MySQL
    // See https://github.com/lagom/lagom/issues/446
    // and https://github.com/slick/slick/issues/1692
    profile match {
      case _: MySQLProfile =>
        MTable.getTables(currentSchema, None, Option("%"), None)
      case _ =>
        MTable.getTables(None, currentSchema, Option("%"), None)
    }

  private def getCurrentSchema: DBIO[Option[String]] =
    SimpleDBIO(ctx => tryGetSchema(ctx.connection).getOrElse(null)).flatMap { schema =>
      if (schema == null) {
        // Not all JDBC drivers support the getSchema method:
        // some always return null.
        // In that case, fall back to vendor-specific queries.
        profile match {
          case _: H2Profile =>
            sql"SELECT SCHEMA();".as[String].headOption
          case _: MySQLProfile =>
            sql"SELECT DATABASE();".as[String].headOption
          case _: PostgresProfile =>
            sql"SELECT current_schema();".as[String].headOption
          case _ =>
            DBIO.successful(None)
        }
      } else DBIO.successful(Some(schema))
    }

  // Some older JDBC drivers don't implement Connection.getSchema
  // (including some builds of H2). This causes them to throw an
  // AbstractMethodError at runtime.
  // Because Try$.apply only catches NonFatal errors, and AbstractMethodError
  // is considered fatal, we need to construct the Try explicitly.
  private def tryGetSchema(connection: Connection): Try[String] =
    try Success(connection.getSchema)
    catch {
      case e: AbstractMethodError =>
        Failure(new IllegalStateException("Database driver does not support Connection.getSchema", e))
    }

  private def tableExists(
      schemaName: Option[String],
      tableName: String
  )(tables: Vector[MTable], currentSchema: Option[String]): Boolean =
    tables.exists { t =>
      profile match {
        case _: MySQLProfile =>
          t.name.catalog.orElse(currentSchema) == schemaName.orElse(currentSchema) && t.name.name == tableName
        case _ =>
          t.name.schema.orElse(currentSchema) == schemaName.orElse(currentSchema) && t.name.name == tableName
      }
    }
}

class ValueEntitySlickCreateTable(override val system: ActorSystem, slickDb: JdbcSlickDatabase)
    extends SlickCreateTables {

  override val profile = slickDb.profile
  override val database = slickDb.database

  import profile.api._

  private val tableCfg = new JdbcEntityTableConfiguration(
    system.settings.config.getConfig("cloudstate.proxy.value-entity.persistence.jdbc")
  )

  private val table = new JdbcEntityTable {
    override val entityTableCfg: JdbcEntityTableConfiguration = tableCfg
    override val profile: JdbcProfile = ValueEntitySlickCreateTable.this.profile
  }

  private val statements = table.EntityTable.schema.createStatements.toSeq

  override val tableConfigurations: Seq[TableConfiguration] =
    Seq(TableConfiguration(statements, tableCfg.schemaName, tableCfg.tableName))
}

class EventSourcedSlickCreateTable(override val system: ActorSystem, slickDb: SlickDatabase) extends SlickCreateTables {

  override val profile = slickDb.profile
  override val database = slickDb.database

  import profile.api._

  private val journalCfg = new JournalTableConfiguration(system.settings.config.getConfig("jdbc-read-journal"))
  private val snapshotCfg = new SnapshotTableConfiguration(
    system.settings.config.getConfig("jdbc-snapshot-store")
  )

  private val journalTables = new JournalTables {
    override val journalTableCfg: JournalTableConfiguration = journalCfg
    override val profile: JdbcProfile = EventSourcedSlickCreateTable.this.profile
  }
  private val snapshotTables = new SnapshotTables {
    override val snapshotTableCfg: SnapshotTableConfiguration = snapshotCfg
    override val profile: JdbcProfile = EventSourcedSlickCreateTable.this.profile
  }

  private val journalStatements =
    profile match {
      case H2Profile =>
        // Work around https://github.com/slick/slick/issues/763
        journalTables.JournalTable.schema.createStatements
          .map(_.replace("GENERATED BY DEFAULT AS IDENTITY(START WITH 1)", "AUTO_INCREMENT"))
          .toSeq
      case MySQLProfile =>
        // Work around https://github.com/slick/slick/issues/1437
        journalTables.JournalTable.schema.createStatements
          .map(_.replace("AUTO_INCREMENT", "AUTO_INCREMENT UNIQUE"))
          .toSeq
      case _ => journalTables.JournalTable.schema.createStatements.toSeq
    }

  private val snapshotStatements = snapshotTables.SnapshotTable.schema.createStatements.toSeq

  override val tableConfigurations: Seq[TableConfiguration] = Seq(
    TableConfiguration(journalStatements, journalCfg.schemaName, journalCfg.tableName),
    TableConfiguration(snapshotStatements, snapshotCfg.schemaName, snapshotCfg.tableName)
  )
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy