From 6fe18d7d778e53958db05910003b416d384c830f Mon Sep 17 00:00:00 2001 From: Naftoli Gugenheim <98384+nafg@users.noreply.github.com> Date: Sun, 29 Sep 2024 04:23:42 -0400 Subject: [PATCH] Upgrade Scala to 2.13 with -Xsource:3 and add scalafmt --- .github/workflows/ci.yml | 10 +- .mergify.yml | 2 +- .scalafmt.conf | 3 + build.sbt | 8 +- src/main/scala/MyPostgresProfile.scala | 267 -------------- .../examples/testkit/MyPostgresProfile.scala | 349 ++++++++++++++++++ src/test/scala/MyPostgresTest.scala | 30 -- .../examples/testkit/MyPostgresTest.scala | 45 +++ 8 files changed, 408 insertions(+), 306 deletions(-) create mode 100644 .scalafmt.conf delete mode 100644 src/main/scala/MyPostgresProfile.scala create mode 100644 src/main/scala/slick/examples/testkit/MyPostgresProfile.scala delete mode 100644 src/test/scala/MyPostgresTest.scala create mode 100644 src/test/scala/slick/examples/testkit/MyPostgresTest.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a946a1..09da1d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.12.19] + scala: [2.13.15] java: [temurin@11] runs-on: ${{ matrix.os }} steps: @@ -67,7 +67,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.12.19] + scala: [2.13.15] java: [temurin@11] runs-on: ${{ matrix.os }} steps: @@ -87,12 +87,12 @@ jobs: - name: Setup sbt uses: sbt/setup-sbt@v1 - - name: Download target directories (2.12.19) + - name: Download target directories (2.13.15) uses: actions/download-artifact@v4 with: - name: target-${{ matrix.os }}-2.12.19-${{ matrix.java }} + name: target-${{ matrix.os }}-2.13.15-${{ matrix.java }} - - name: Inflate target directories (2.12.19) + - name: Inflate target directories (2.13.15) run: | tar xf targets.tar rm targets.tar diff --git a/.mergify.yml b/.mergify.yml index 9236fe1..88aa524 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -9,7 +9,7 @@ pull_request_rules: - author=scala-steward - author=slick-scala-steward[bot] - author=renovate[bot] - - check-success=Build and Test (ubuntu-latest, 2.12.19, temurin@11) + - check-success=Build and Test (ubuntu-latest, 2.13.15, temurin@11) actions: queue: name: default diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..1b7ca58 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,3 @@ +version = 3.8.3 +runner.dialect = scala3 +align.preset = most diff --git a/build.sbt b/build.sbt index 0ea33ae..d3fde55 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,7 @@ import _root_.io.github.nafg.mergify.dsl.* +ThisBuild / scalaVersion := "2.13.15" +ThisBuild / scalacOptions += "-Xsource:3" mergifyExtraConditions := Seq( (Attr.Author :== "scala-steward") || @@ -9,8 +11,8 @@ mergifyExtraConditions := Seq( libraryDependencies ++= List( "com.github.sbt" % "junit-interface" % "0.13.3" % Test, - "ch.qos.logback" % "logback-classic" % "1.5.8" % Test, - "org.postgresql" % "postgresql" % "42.7.4" % Test, + "ch.qos.logback" % "logback-classic" % "1.5.8" % Test, + "org.postgresql" % "postgresql" % "42.7.4" % Test ) scalacOptions += "-deprecation" @@ -23,7 +25,7 @@ run / fork := true testOptions += Tests.Argument(TestFrameworks.JUnit, "-q", "-v", "-s", "-a") libraryDependencies += "com.typesafe.slick" %% "slick-testkit" % "3.5.1" -libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value +libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value ThisBuild / githubWorkflowJavaVersions := Seq(JavaSpec.temurin("11")) ThisBuild / githubWorkflowBuildPreamble += diff --git a/src/main/scala/MyPostgresProfile.scala b/src/main/scala/MyPostgresProfile.scala deleted file mode 100644 index 802ea64..0000000 --- a/src/main/scala/MyPostgresProfile.scala +++ /dev/null @@ -1,267 +0,0 @@ -//#outline -package com.example - -//#outline -import java.sql.{PreparedStatement, ResultSet} -import java.util.UUID - -import scala.collection.mutable -import scala.concurrent.ExecutionContext - -import slick.ast._ -import slick.basic.Capability -import slick.compiler.{CompilerState, Phase} -import slick.dbio._ -import slick.jdbc._ -import slick.jdbc.meta.{MColumn, MIndexInfo, MTable} -import slick.relational.RelationalProfile -import slick.util.ConstArray -import slick.util.QueryInterpolator.queryInterpolator - - -/** Slick profile for PostgreSQL. - * - * This profile implements [[slick.jdbc.JdbcProfile]] - * ''without'' the following capabilities: - * - * - * - * Notes: - * - * - */ -//#outline -trait MyPostgresProfile extends JdbcProfile with JdbcActionComponent.MultipleRowsPerStatementSupport { - // ... - //#outline - - override protected def computeCapabilities: Set[Capability] = (super.computeCapabilities - - JdbcCapabilities.insertOrUpdate - - JdbcCapabilities.insertOrUpdateWithPrimaryKeyOnly - - JdbcCapabilities.nullableNoDefault - - JdbcCapabilities.supportsByte - ) - - class ModelBuilder(mTables: Seq[MTable], ignoreInvalidDefaults: Boolean)(implicit ec: ExecutionContext) - extends JdbcModelBuilder(mTables, ignoreInvalidDefaults) { - override def createTableNamer(mTable: MTable): TableNamer = new TableNamer(mTable) { - override def schema = super.schema.filter(_ != "public") // remove default schema - } - override def createColumnBuilder(tableBuilder: TableBuilder, meta: MColumn): ColumnBuilder = - new ColumnBuilder(tableBuilder, meta) { - val NumericPattern = "^['(]?(-?[0-9]+\\.?[0-9]*)[')]?(?:::(?:numeric|bigint|integer))?".r - val TextPattern = "^'(.*)'::(?:bpchar|character varying|text)".r - val UUIDPattern = "^'(.*)'::uuid".r - override def default = meta.columnDef.map((_, tpe)).collect { - case ("true", "Boolean") => Some(Some(true)) - case ("false", "Boolean") => Some(Some(false)) - case (TextPattern(str), "String") => Some(Some(str)) - case ("NULL::bpchar", "String") => Some(None) - case (TextPattern(str), "Char") => str.length match { - case 0 => Some(Some(' ')) // Default to one space, as the char will be space padded anyway - case 1 => Some(Some(str.head)) - case _ => None // This is invalid, so let's not supply any default - } - case ("NULL::bpchar", "Char") => Some(None) - case (NumericPattern(v), "Short") => Some(Some(v.toShort)) - case (NumericPattern(v), "Int") => Some(Some(v.toInt)) - case (NumericPattern(v), "Long") => Some(Some(v.toLong)) - case (NumericPattern(v), "Float") => Some(Some(v.toFloat)) - case (NumericPattern(v), "Double") => Some(Some(v.toDouble)) - case (NumericPattern(v), "scala.math.BigDecimal") => Some(Some(BigDecimal(s"$v"))) - case (UUIDPattern(v), "java.util.UUID") => Some(Some(java.util.UUID.fromString(v))) - case (_, "java.util.UUID") => - None // The UUID is generated through a function - treat it as if there was no default. - }.getOrElse { - val d = super.default - if (meta.nullable.contains(true) && d.isEmpty) { - Some(None) - } else d - } - override def length: Option[Int] = { - val l = super.length - if (tpe == "String" && varying && l.contains(2147483647)) None - else l - } - override def tpe = meta.typeName match { - case "bytea" => "Array[Byte]" - case "lo" if meta.sqlType == java.sql.Types.DISTINCT => "java.sql.Blob" - case "uuid" => "java.util.UUID" - case _ => super.tpe - } - } - override def createIndexBuilder(tableBuilder: TableBuilder, meta: Seq[MIndexInfo]): IndexBuilder = - new IndexBuilder(tableBuilder, meta) { - override def columns = super.columns.map(_.stripPrefix("\"").stripSuffix("\"")) - } - } - - override def createModelBuilder(tables: Seq[MTable], ignoreInvalidDefaults: Boolean) - (implicit ec: ExecutionContext): JdbcModelBuilder = - new ModelBuilder(tables, ignoreInvalidDefaults) - - override def defaultTables(implicit ec: ExecutionContext): DBIO[Seq[MTable]] = - MTable.getTables(None, None, None, Some(Seq("TABLE"))) - - override val columnTypes = new JdbcTypes - override protected def computeQueryCompiler = super.computeQueryCompiler - Phase.rewriteDistinct - override def createQueryBuilder(n: Node, state: CompilerState): QueryBuilder = new QueryBuilder(n, state) - override def createUpsertBuilder(node: Insert): InsertBuilder = new UpsertBuilder(node) - override def createTableDDLBuilder(table: Table[_]): TableDDLBuilder = new TableDDLBuilder(table) - override def createColumnDDLBuilder(column: FieldSymbol, table: Table[_]): ColumnDDLBuilder = - new ColumnDDLBuilder(column) - override protected lazy val useServerSideUpsert = true - override protected lazy val useTransactionForUpsert = true - override protected lazy val useServerSideUpsertReturning = false - - override def defaultSqlTypeName(tmd: JdbcType[_], sym: Option[FieldSymbol]): String = tmd.sqlType match { - case java.sql.Types.VARCHAR => - val size = sym.flatMap(_.findColumnOption[RelationalProfile.ColumnOption.Length]) - size.fold("VARCHAR")(l => if (l.varying) s"VARCHAR(${l.length})" else s"CHAR(${l.length})") - case java.sql.Types.BLOB => "lo" - case java.sql.Types.DOUBLE => "DOUBLE PRECISION" - /* PostgreSQL does not have a TINYINT type, so we use SMALLINT instead. */ - case java.sql.Types.TINYINT => "SMALLINT" - case _ => super.defaultSqlTypeName(tmd, sym) - } - - class QueryBuilder(tree: Node, state: CompilerState) extends super.QueryBuilder(tree, state) { - override protected val concatOperator = Some("||") - override protected val quotedJdbcFns = Some(Vector(Library.Database, Library.User)) - - override protected def buildSelectModifiers(c: Comprehension.Base): Unit = (c.distinct, c.select) match { - case (Some(ProductNode(onNodes)), Pure(ProductNode(selNodes), _)) if onNodes.nonEmpty => - def eligible(a: ConstArray[Node]) = a.forall { - case _: PathElement => true - case _: LiteralNode => true - case _: QueryParameter => true - case _ => false - } - if (eligible(onNodes) && eligible(selNodes) && - onNodes.iterator.collect[List[TermSymbol]] { case FwdPath(ss) => ss }.toSet == - selNodes.iterator.collect[List[TermSymbol]] { case FwdPath(ss) => ss }.toSet - ) b"distinct " else super.buildSelectModifiers(c) - case _ => super - .buildSelectModifiers(c) - } - - override protected def buildFetchOffsetClause(fetch: Option[Node], offset: Option[Node]) = (fetch, offset) match { - case (Some(t), Some(d)) => b"\nlimit $t offset $d" - case (Some(t), None) => b"\nlimit $t" - case (None, Some(d)) => b"\noffset $d" - case _ => - } - - override def expr(n: Node) = n match { - case Library.UCase(ch) => b"upper($ch)" - case Library.LCase(ch) => b"lower($ch)" - case Library.IfNull(ch, d) => b"coalesce($ch, $d)" - case Library.NextValue(SequenceNode(name)) => b"nextval('$name')" - case Library.CurrentValue(SequenceNode(name)) => b"currval('$name')" - case Library.CurrentDate() => b"current_date" - case Library.CurrentTime() => b"current_time" - case _ => super.expr(n) - } - } - - class UpsertBuilder(ins: Insert) extends super.UpsertBuilder(ins) { - override def buildInsert: InsertBuilderResult = { - val update = - "update " + tableName + " set " + softNames.map(n => s"$n=?").mkString(",") + - " where " + pkNames.map(n => s"$n=?").mkString(" and ") - val nonAutoIncNames = nonAutoIncSyms.map(fs => quoteIdentifier(fs.name)).mkString(",") - val nonAutoIncVars = nonAutoIncSyms.map(_ => "?").mkString(",") - val cond = pkNames.map(n => s"$n=?").mkString(" and ") - val insert = - s"insert into $tableName ($nonAutoIncNames) select $nonAutoIncVars" + - s" where not exists (select 1 from $tableName where $cond)" - new InsertBuilderResult(table, s"$update; $insert", ConstArray.from(softSyms ++ pkSyms)) - } - - override def transformMapping(n: Node) = reorderColumns(n, softSyms ++ pkSyms ++ nonAutoIncSyms.toSeq ++ pkSyms) - } - - class TableDDLBuilder(table: Table[_]) extends super.TableDDLBuilder(table) { - override def createPhase1 = super.createPhase1 ++ columns.flatMap { - case cb: ColumnDDLBuilder => cb.createLobTrigger(table.tableName) - } - override def dropPhase1 = { - val dropLobs = columns.flatMap { - case cb: ColumnDDLBuilder => cb.dropLobTrigger(table.tableName) - } - if (dropLobs.isEmpty) super.dropPhase1 - else Seq("delete from " + quoteIdentifier(table.tableName)) ++ dropLobs ++ super.dropPhase1 - } - } - - class ColumnDDLBuilder(column: FieldSymbol) extends super.ColumnDDLBuilder(column) { - override def appendColumn(sb: mutable.StringBuilder): Unit = { - sb append quoteIdentifier(column.name) append ' ' - if (autoIncrement && !customSqlType) { - sb append (if (sqlType.toUpperCase == "BIGINT") "BIGSERIAL" else "SERIAL") - } else appendType(sb) - autoIncrement = false - appendOptions(sb) - } - - def lobTrigger(tName: String) = - quoteIdentifier(tName + "__" + quoteIdentifier(column.name) + "_lob") - - def createLobTrigger(tName: String): Option[String] = - if (sqlType == "lo") Some( - "create trigger " + lobTrigger(tName) + " before update or delete on " + - quoteIdentifier(tName) + " for each row execute procedure lo_manage(" + quoteIdentifier(column.name) + ")" - ) else None - - def dropLobTrigger(tName: String): Option[String] = - if (sqlType == "lo") Some( - "drop trigger " + lobTrigger(tName) + " on " + quoteIdentifier(tName) - ) else None - } - - class JdbcTypes extends super.JdbcTypes { - override val byteArrayJdbcType = new ByteArrayJdbcType - override val uuidJdbcType = new UUIDJdbcType - - class ByteArrayJdbcType extends super.ByteArrayJdbcType { - override val sqlType = java.sql.Types.BINARY - override def sqlTypeName(sym: Option[FieldSymbol]) = "BYTEA" - } - - class UUIDJdbcType extends super.UUIDJdbcType { - override def sqlTypeName(sym: Option[FieldSymbol]) = "UUID" - override def setValue(v: UUID, p: PreparedStatement, idx: Int) = p.setObject(idx, v, sqlType) - override def getValue(r: ResultSet, idx: Int) = r.getObject(idx).asInstanceOf[UUID] - override def updateValue(v: UUID, r: ResultSet, idx: Int) = r.updateObject(idx, v) - override def valueToSQLLiteral(value: UUID) = "'" + value + "'" - override def hasLiteralForm = true - } - } - //#outline -} - -object MyPostgresProfile extends MyPostgresProfile -//#outline diff --git a/src/main/scala/slick/examples/testkit/MyPostgresProfile.scala b/src/main/scala/slick/examples/testkit/MyPostgresProfile.scala new file mode 100644 index 0000000..c9db836 --- /dev/null +++ b/src/main/scala/slick/examples/testkit/MyPostgresProfile.scala @@ -0,0 +1,349 @@ +//#outline +package slick.examples.testkit + +//#outline +import java.sql.{PreparedStatement, ResultSet} +import java.util.UUID + +import scala.collection.mutable +import scala.concurrent.ExecutionContext + +import slick.ast.* +import slick.basic.Capability +import slick.compiler.{CompilerState, Phase} +import slick.dbio.* +import slick.jdbc.* +import slick.jdbc.meta.{MColumn, MIndexInfo, MTable} +import slick.relational.RelationalProfile +import slick.util.ConstArray +import slick.util.QueryInterpolator.queryInterpolator + +/** Slick profile for PostgreSQL. + * + * This profile implements [[slick.jdbc.JdbcProfile]] ''without'' the following + * capabilities: + * + * + * + * Notes: + * + * + */ +//#outline +trait MyPostgresProfile + extends JdbcProfile + with JdbcActionComponent.MultipleRowsPerStatementSupport { + // ... + // #outline + + override protected def computeCapabilities: Set[Capability] = + super.computeCapabilities - + JdbcCapabilities.insertOrUpdate - + JdbcCapabilities.insertOrUpdateWithPrimaryKeyOnly - + JdbcCapabilities.nullableNoDefault - + JdbcCapabilities.supportsByte + + private class ModelBuilder( + mTables: Seq[MTable], + ignoreInvalidDefaults: Boolean + )(implicit + ec: ExecutionContext + ) extends JdbcModelBuilder(mTables, ignoreInvalidDefaults) { + override def createTableNamer(mTable: MTable): TableNamer = new TableNamer( + mTable + ) { + override def schema = + super.schema.filter(_ != "public") // remove default schema + } + override def createColumnBuilder( + tableBuilder: TableBuilder, + meta: MColumn + ): ColumnBuilder = + new ColumnBuilder(tableBuilder, meta) { + val NumericPattern = + "^['(]?(-?[0-9]+\\.?[0-9]*)[')]?(?:::(?:numeric|bigint|integer))?".r + val TextPattern = "^'(.*)'::(?:bpchar|character varying|text)".r + val UUIDPattern = "^'(.*)'::uuid".r + override def default = meta.columnDef + .map((_, tpe)) + .collect { + case ("true", "Boolean") => Some(Some(true)) + case ("false", "Boolean") => Some(Some(false)) + case (TextPattern(str), "String") => Some(Some(str)) + case ("NULL::bpchar", "String") => Some(None) + case (TextPattern(str), "Char") => + str.length match { + case 0 => + Some( + Some(' ') + ) // Default to one space, as the char will be space padded anyway + case 1 => Some(Some(str.head)) + case _ => + None // This is invalid, so let's not supply any default + } + case ("NULL::bpchar", "Char") => Some(None) + case (NumericPattern(v), "Short") => Some(Some(v.toShort)) + case (NumericPattern(v), "Int") => Some(Some(v.toInt)) + case (NumericPattern(v), "Long") => Some(Some(v.toLong)) + case (NumericPattern(v), "Float") => Some(Some(v.toFloat)) + case (NumericPattern(v), "Double") => Some(Some(v.toDouble)) + case (NumericPattern(v), "scala.math.BigDecimal") => + Some(Some(BigDecimal(s"$v"))) + case (UUIDPattern(v), "java.util.UUID") => + Some(Some(java.util.UUID.fromString(v))) + case (_, "java.util.UUID") => + None // The UUID is generated through a function - treat it as if there was no default. + } + .getOrElse { + val d = super.default + if (meta.nullable.contains(true) && d.isEmpty) { + Some(None) + } else d + } + override def length: Option[Int] = { + val l = super.length + if (tpe == "String" && varying && l.contains(2147483647)) None + else l + } + override def tpe = meta.typeName match { + case "bytea" => "Array[Byte]" + case "lo" if meta.sqlType == java.sql.Types.DISTINCT => + "java.sql.Blob" + case "uuid" => "java.util.UUID" + case _ => super.tpe + } + } + override def createIndexBuilder( + tableBuilder: TableBuilder, + meta: Seq[MIndexInfo] + ): IndexBuilder = + new IndexBuilder(tableBuilder, meta) { + override def columns = + super.columns.map(_.stripPrefix("\"").stripSuffix("\"")) + } + } + + override def createModelBuilder( + tables: Seq[MTable], + ignoreInvalidDefaults: Boolean + )(implicit ec: ExecutionContext): JdbcModelBuilder = + new ModelBuilder(tables, ignoreInvalidDefaults) + + override def defaultTables(implicit ec: ExecutionContext): DBIO[Seq[MTable]] = + MTable.getTables(None, None, None, Some(Seq("TABLE"))) + + override val columnTypes: MyJdbcTypes = new MyJdbcTypes + override protected def computeQueryCompiler = + super.computeQueryCompiler - Phase.rewriteDistinct + override def createQueryBuilder( + n: Node, + state: CompilerState + ): MyQueryBuilder = + new MyQueryBuilder(n, state) + override def createUpsertBuilder(node: Insert): InsertBuilder = + new MyUpsertBuilder(node) + override def createTableDDLBuilder(table: Table[?]): MyTableDDLBuilder = + new MyTableDDLBuilder(table) + override def createColumnDDLBuilder( + column: FieldSymbol, + table: Table[?] + ): MyColumnDDLBuilder = + new MyColumnDDLBuilder(column) + override protected lazy val useServerSideUpsert = true + override protected lazy val useTransactionForUpsert = true + override protected lazy val useServerSideUpsertReturning = false + + override def defaultSqlTypeName( + tmd: JdbcType[?], + sym: Option[FieldSymbol] + ): String = tmd.sqlType match { + case java.sql.Types.VARCHAR => + val size = + sym.flatMap(_.findColumnOption[RelationalProfile.ColumnOption.Length]) + size.fold("VARCHAR")(l => + if (l.varying) s"VARCHAR(${l.length})" else s"CHAR(${l.length})" + ) + case java.sql.Types.BLOB => "lo" + case java.sql.Types.DOUBLE => "DOUBLE PRECISION" + /* PostgreSQL does not have a TINYINT type, so we use SMALLINT instead. */ + case java.sql.Types.TINYINT => "SMALLINT" + case _ => super.defaultSqlTypeName(tmd, sym) + } + + class MyQueryBuilder(tree: Node, state: CompilerState) + extends QueryBuilder(tree, state) { + override protected val concatOperator: Option[String] = Some("||") + override protected val quotedJdbcFns: Option[Seq[Library.JdbcFunction]] = + Some( + Vector(Library.Database, Library.User) + ) + + override protected def buildSelectModifiers(c: Comprehension.Base): Unit = + (c.distinct, c.select) match { + case (Some(ProductNode(onNodes)), Pure(ProductNode(selNodes), _)) + if onNodes.nonEmpty => + def eligible(a: ConstArray[Node]) = a.forall { + case _: PathElement => true + case _: LiteralNode => true + case _: QueryParameter => true + case _ => false + } + if ( + eligible(onNodes) && eligible(selNodes) && + onNodes.iterator + .collect[List[TermSymbol]] { case FwdPath(ss) => ss } + .toSet == + selNodes.iterator + .collect[List[TermSymbol]] { case FwdPath(ss) => ss } + .toSet + ) b"distinct " + else super.buildSelectModifiers(c) + case _ => + super + .buildSelectModifiers(c) + } + + override protected def buildFetchOffsetClause( + fetch: Option[Node], + offset: Option[Node] + ) = (fetch, offset) match { + case (Some(t), Some(d)) => b"\nlimit $t offset $d" + case (Some(t), None) => b"\nlimit $t" + case (None, Some(d)) => b"\noffset $d" + case _ => + } + + override def expr(n: Node) = n match { + case Library.UCase(ch) => b"upper($ch)" + case Library.LCase(ch) => b"lower($ch)" + case Library.IfNull(ch, d) => b"coalesce($ch, $d)" + case Library.NextValue(SequenceNode(name)) => b"nextval('$name')" + case Library.CurrentValue(SequenceNode(name)) => b"currval('$name')" + case Library.CurrentDate() => b"current_date" + case Library.CurrentTime() => b"current_time" + case _ => super.expr(n) + } + } + + private class MyUpsertBuilder(ins: Insert) extends UpsertBuilder(ins) { + override def buildInsert: InsertBuilderResult = { + val update = + "update " + tableName + " set " + softNames + .map(n => s"$n=?") + .mkString(",") + + " where " + pkNames.map(n => s"$n=?").mkString(" and ") + val nonAutoIncNames = + nonAutoIncSyms.map(fs => quoteIdentifier(fs.name)).mkString(",") + val nonAutoIncVars = nonAutoIncSyms.map(_ => "?").mkString(",") + val cond = pkNames.map(n => s"$n=?").mkString(" and ") + val insert = + s"insert into $tableName ($nonAutoIncNames) select $nonAutoIncVars" + + s" where not exists (select 1 from $tableName where $cond)" + new InsertBuilderResult( + table, + s"$update; $insert", + ConstArray.from(softSyms ++ pkSyms) + ) + } + + override def transformMapping(n: Node) = + reorderColumns(n, softSyms ++ pkSyms ++ nonAutoIncSyms.toSeq ++ pkSyms) + } + + class MyTableDDLBuilder(table: Table[?]) extends TableDDLBuilder(table) { + override def createPhase1 = super.createPhase1 ++ columns.flatMap { + case cb: MyColumnDDLBuilder => cb.createLobTrigger(table.tableName) + } + override def dropPhase1 = { + val dropLobs = columns.flatMap { case cb: MyColumnDDLBuilder => + cb.dropLobTrigger(table.tableName) + } + if (dropLobs.isEmpty) super.dropPhase1 + else + Seq( + "delete from " + quoteIdentifier(table.tableName) + ) ++ dropLobs ++ super.dropPhase1 + } + } + + class MyColumnDDLBuilder(column: FieldSymbol) + extends ColumnDDLBuilder(column) { + override def appendColumn(sb: mutable.StringBuilder): Unit = { + sb append quoteIdentifier(column.name) append ' ' + if (autoIncrement && !customSqlType) { + sb append (if (sqlType.toUpperCase == "BIGINT") "BIGSERIAL" + else "SERIAL") + } else appendType(sb) + autoIncrement = false + appendOptions(sb) + } + + private def lobTrigger(tName: String) = + quoteIdentifier(tName + "__" + quoteIdentifier(column.name) + "_lob") + + def createLobTrigger(tName: String): Option[String] = + if (sqlType == "lo") + Some( + "create trigger " + lobTrigger( + tName + ) + " before update or delete on " + + quoteIdentifier( + tName + ) + " for each row execute procedure lo_manage(" + quoteIdentifier( + column.name + ) + ")" + ) + else None + + def dropLobTrigger(tName: String): Option[String] = + if (sqlType == "lo") + Some( + "drop trigger " + lobTrigger(tName) + " on " + quoteIdentifier(tName) + ) + else None + } + + class MyJdbcTypes extends JdbcTypes { + override val byteArrayJdbcType: MyByteArrayJdbcType = + new MyByteArrayJdbcType + override val uuidJdbcType: MyUUIDJdbcType = new MyUUIDJdbcType + + class MyByteArrayJdbcType extends ByteArrayJdbcType { + override val sqlType = java.sql.Types.BINARY + override def sqlTypeName(sym: Option[FieldSymbol]) = "BYTEA" + } + + class MyUUIDJdbcType extends UUIDJdbcType { + override def sqlTypeName(sym: Option[FieldSymbol]) = "UUID" + override def setValue(v: UUID, p: PreparedStatement, idx: Int) = + p.setObject(idx, v, sqlType) + override def getValue(r: ResultSet, idx: Int) = + r.getObject(idx).asInstanceOf[UUID] + override def updateValue(v: UUID, r: ResultSet, idx: Int) = + r.updateObject(idx, v) + override def valueToSQLLiteral(value: UUID) = "'" + value + "'" + override def hasLiteralForm = true + } + } + // #outline +} + +object MyPostgresProfile extends MyPostgresProfile +//#outline diff --git a/src/test/scala/MyPostgresTest.scala b/src/test/scala/MyPostgresTest.scala deleted file mode 100644 index 933f029..0000000 --- a/src/test/scala/MyPostgresTest.scala +++ /dev/null @@ -1,30 +0,0 @@ -package com.example - -import org.junit.runner.RunWith -import com.typesafe.slick.testkit.util.{ExternalJdbcTestDB, TestDB, ProfileTest, Testkit} -import scala.concurrent.ExecutionContext -import slick.jdbc.ResultSetAction -import slick.dbio.DBIO -import slick.jdbc.GetResult._ - -//#outline -@RunWith(classOf[Testkit]) -class MyPostgresTest extends ProfileTest(MyPostgresTest.tdb) - -object MyPostgresTest { - //#tdb - def tdb = new ExternalJdbcTestDB("mypostgres") { - //#tdb - val profile = MyPostgresProfile - override def localTables(implicit ec: ExecutionContext): DBIO[Vector[String]] = - ResultSetAction[(String,String,String, String)](_.conn.getMetaData().getTables("", "public", null, null)).map { ts => - ts.filter(_._4.toUpperCase == "TABLE").map(_._3).sorted - } - override def localSequences(implicit ec: ExecutionContext): DBIO[Vector[String]] = - ResultSetAction[(String,String,String, String)](_.conn.getMetaData().getTables("", "public", null, null)).map { ts => - ts.filter(_._4.toUpperCase == "SEQUENCE").map(_._3).sorted - } - override def capabilities = super.capabilities - TestDB.capabilities.jdbcMetaGetFunctions - } -} -//#outline diff --git a/src/test/scala/slick/examples/testkit/MyPostgresTest.scala b/src/test/scala/slick/examples/testkit/MyPostgresTest.scala new file mode 100644 index 0000000..e924d19 --- /dev/null +++ b/src/test/scala/slick/examples/testkit/MyPostgresTest.scala @@ -0,0 +1,45 @@ +package slick.examples.testkit + +import org.junit.runner.RunWith +import com.typesafe.slick.testkit.util.{ + ExternalJdbcTestDB, + ProfileTest, + TestDB, + Testkit +} +import scala.concurrent.ExecutionContext + +import slick.jdbc.ResultSetAction +import slick.dbio.DBIO +import slick.jdbc.GetResult.* + +//#outline +@RunWith(classOf[Testkit]) +class MyPostgresTest extends ProfileTest(MyPostgresTest.tdb) + +object MyPostgresTest { + // #tdb + def tdb = new ExternalJdbcTestDB("mypostgres") { + // #tdb + val profile: MyPostgresProfile.type = MyPostgresProfile + override def localTables(implicit + ec: ExecutionContext + ): DBIO[Vector[String]] = + ResultSetAction[(String, String, String, String)]( + _.conn.getMetaData.getTables("", "public", null, null) + ).map { ts => + ts.filter(_._4.toUpperCase == "TABLE").map(_._3).sorted + } + override def localSequences(implicit + ec: ExecutionContext + ): DBIO[Vector[String]] = + ResultSetAction[(String, String, String, String)]( + _.conn.getMetaData.getTables("", "public", null, null) + ).map { ts => + ts.filter(_._4.toUpperCase == "SEQUENCE").map(_._3).sorted + } + override def capabilities = + super.capabilities - TestDB.capabilities.jdbcMetaGetFunctions + } +} +//#outline