Skip to content
Permalink
Browse files

Proper support for string lengths:

- Allow specifying the basic type in O.DBType plus a separate length
  in O.Length.

- Make TEXT the default String type on MySQL. This may break some code
  which used String columns without any further DBType or Length option
  because TEXT cannot have a default value or be (easily, in the
  Slick-supported way) indexed.

- Make the default String type for MySQL configurable through
  application.conf (or by subclassing the driver and providing a custom
  configuration).

- Recognize unconstrained TEXT (on MySQL) and VARCHAR (on PostgreSQL)
  types in the model builder and omit O.Length in these cases.

- Remove the type bound on O.Length because it has to be usable for
  String, Option[String] and any mapped type based on these two.

Some test cases changed to specify O.Length(254) explicitly where needed
for MySQL. JdbcMiscTest.testColumnOptions removed (no longer
applicable).

Fixes #975.
  • Loading branch information
szeiger committed Feb 5, 2015
1 parent ac5a097 commit cc8d3f0c76b10ddf74a6cd35436e3a41956ae284
@@ -234,7 +234,7 @@ class Tables(val profile: JdbcProfile){
case class Category(id: Int, name: String)
class Categories(tag: Tag) extends Table[Category](tag, "categories") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def name = column[String]("name", O.Length(254))
def * = (id, name) <> (Category.tupled,Category.unapply)
def idx = index("IDX_NAME",name)
}
@@ -267,7 +267,7 @@ class Tables(val profile: JdbcProfile){
def Float = column[Float]("Float",O.Default(9.999F))
def Double = column[Double]("Double",O.Default(9.999))
//def java_math_BigDecimal = column[java.math.BigDecimal]("java_math_BigDecimal")
def String = column[String]("String",O.Default("someDefaultString"))
def String = column[String]("String",O.Default("someDefaultString"),O.Length(254))
def java_sql_Date = column[java.sql.Date]("java_sql_Date")
def java_sql_Time = column[java.sql.Time]("java_sql_Time")
def java_sql_Timestamp = column[java.sql.Timestamp]("java_sql_Timestamp")
@@ -285,7 +285,7 @@ class Tables(val profile: JdbcProfile){
def Option_Float = column[Option[Float]]("Option_Float",O.Default(Some(9.999F)))
def Option_Double = column[Option[Double]]("Option_Double",O.Default(Some(9.999)))
//def java_math_BigDecimal = column[Option[java.math.BigDecimal]]("java_math_BigDecimal")
def Option_String = column[Option[String]]("Option_String",O.Default(Some("someDefaultString")))
def Option_String = column[Option[String]]("Option_String",O.Default(Some("someDefaultString")),O.Length(254))
def Option_java_sql_Date = column[Option[java.sql.Date]]("Option_java_sql_Date")
def Option_java_sql_Time = column[Option[java.sql.Time]]("Option_java_sql_Time")
def Option_java_sql_Timestamp = column[Option[java.sql.Timestamp]]("Option_java_sql_Timestamp")
@@ -363,4 +363,3 @@ class Tables(val profile: JdbcProfile){
}
val large = TableQuery[Large]
}

@@ -7,7 +7,7 @@ class ColumnDefaultTest extends AsyncTest[RelationalTestDB] {

class A(tag: Tag) extends Table[(Int, String, Option[Boolean])](tag, "a") {
def id = column[Int]("id")
def a = column[String]("a", O Default "foo")
def a = column[String]("a", O Default "foo", O Length 254)
def b = column[Option[Boolean]]("b", O Default Some(true))
def * = (id, a, b)
}
@@ -9,7 +9,7 @@ class IterateeTest extends TestkitTest[JdbcTestDB] {
import tdb.profile.simple._

class A(tag: Tag) extends Table[(String, Int)](tag, "a") {
def s = column[String]("s", O.PrimaryKey)
def s = column[String]("s", O.PrimaryKey, O.Length(254))
def i = column[Int]("i")
def * = (s, i)
}
@@ -46,14 +46,6 @@ class JdbcMiscTest extends AsyncTest[JdbcTestDB] {
)
}

def testColumnOptions = {
class Foo(tag: Tag) extends Table[String](tag, "posts") {
def bar = column[String]("s", O.Length(20,varying=true), O SqlType "VARCHAR(20)" )
def * = bar
}
Action.successful(()).flatMap { _ => TableQuery[Foo].schema.create }.failed.map(_.shouldBeA[SlickException])
}

def testSimpleDBIO = {
val getAutoCommit = SimpleDBIO[Boolean](_.connection.getAutoCommit)
getAutoCommit.map(_ shouldBe true)
@@ -45,12 +45,12 @@ class ModelBuilderTest extends TestkitTest[JdbcTestDB] {
def someBoolOptionDefaultSome = column[Option[Boolean]]("some_bool_option_default_some",O.Default(Some(true)))
def someBoolOptionDefaultNone = column[Option[Boolean]]("some_bool_option_default_none",O.Default(None))
def someString = column[String]("some_string")
def someStringDefaultNonEmpty = column[String]("some_string_default_non_empty",O.Default("bar"))
def someStringDefaultEmpty = column[String]("some_string_default_empty",O.Default(""))
def someStringDefaultNonEmpty = column[String]("some_string_default_non_empty",O.Default("bar"),O.Length(254))
def someStringDefaultEmpty = column[String]("some_string_default_empty",O.Default(""),O.Length(254))
def someStringOption = column[Option[String]]("some_string_option")
def someStringOptionDefaultEmpty = column[Option[String]]("str_option_default_empty",O.Default(Some("")))
def someStringOptionDefaultEmpty = column[Option[String]]("str_option_default_empty",O.Default(Some("")),O.Length(254))
def someStringOptionDefaultNone = column[Option[String]]("str_option_default_none",O.Default(None))
def someStringOptionDefaultNonEmpty = column[Option[String]]("str_option_default_non_empty",O.Default(Some("foo")))
def someStringOptionDefaultNonEmpty = column[Option[String]]("str_option_default_non_empty",O.Default(Some("foo")),O.Length(254))
def * = (someBool,someBoolDefaultTrue,someBoolDefaultFalse,someBoolOption,someBoolOptionDefaultSome,someBoolOptionDefaultNone,someString,someStringDefaultNonEmpty,someStringDefaultEmpty,someStringOption,someStringOptionDefaultEmpty,someStringOptionDefaultNonEmpty,someStringOptionDefaultNone)
}
val defaultTest = TableQuery[DefaultTest]
@@ -79,7 +79,7 @@ class ModelBuilderTest extends TestkitTest[JdbcTestDB] {
def Float = column[Float]("Float",O.Default(9.999F))
def Double = column[Double]("Double",O.Default(9.999))
//def java_math_BigDecimal = column[java.math.BigDecimal]("java_math_BigDecimal")
def String = column[String]("String",O.Default("someDefaultString"))
def String = column[String]("String",O.Default("someDefaultString"), O.Length(254))
def java_sql_Date = column[java.sql.Date]("java_sql_Date")
def java_sql_Time = column[java.sql.Time]("java_sql_Time")
def java_sql_Timestamp = column[java.sql.Timestamp]("java_sql_Timestamp")
@@ -97,7 +97,7 @@ class ModelBuilderTest extends TestkitTest[JdbcTestDB] {
def Option_Float = column[Option[Float]]("Option_Float",O.Default(Some(9.999F)))
def Option_Double = column[Option[Double]]("Option_Double",O.Default(Some(9.999)))
//def java_math_BigDecimal = column[Option[java.math.BigDecimal]]("java_math_BigDecimal")
def Option_String = column[Option[String]]("Option_String",O.Default(Some("someDefaultString")))
def Option_String = column[Option[String]]("Option_String",O.Default(Some("someDefaultString")), O.Length(254))
def Option_java_sql_Date = column[Option[java.sql.Date]]("Option_java_sql_Date")
def Option_java_sql_Time = column[Option[java.sql.Time]]("Option_java_sql_Time")
def Option_java_sql_Timestamp = column[Option[java.sql.Timestamp]]("Option_java_sql_Timestamp")
@@ -19,7 +19,7 @@ class NewQuerySemanticsTest extends AsyncTest[RelationalTestDB] {
val suppliersStd = TableQuery[SuppliersStd]

class CoffeesStd(tag: Tag) extends Table[(String, Int, Int, Int, Int)](tag, "COFFEES") {
def name = column[String]("COF_NAME", O.PrimaryKey)
def name = column[String]("COF_NAME", O.PrimaryKey, O.Length(254))
def supID = column[Int]("SUP_ID")
def price = column[Int]("PRICE")
def sales = column[Int]("SALES")
@@ -18,7 +18,7 @@ class CodeGeneratorAllTest(val tdb: JdbcTestDB) extends DBTest {
case class Category(id: Int, name: String)
class Categories(tag: Tag) extends Table[Category](tag, "categories") {
def id = column[Int]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def name = column[String]("name", O.Length(254))
def * = (id, name) <> (Category.tupled,Category.unapply)
def idx = index("IDX_NAME",name)
}
@@ -12,3 +12,8 @@ slick {
# Use multi-line, indented formatting for SQL statements
sqlIndent = false
}

slick.driver.MySQL {
# The default SQL type for strings without an explicit size limit
defaultStringType = "TEXT"
}
@@ -106,13 +106,13 @@ trait AccessDriver extends JdbcDriver { driver =>
override def createTableDDLBuilder(table: Table[_]): TableDDLBuilder = new TableDDLBuilder(table)
override def createColumnDDLBuilder(column: FieldSymbol, table: Table[_]): ColumnDDLBuilder = new ColumnDDLBuilder(column)

override def defaultSqlTypeName(tmd: JdbcType[_]): String = tmd.sqlType match {
override def defaultSqlTypeName(tmd: JdbcType[_], size: Option[RelationalProfile.ColumnOption.Length]): String = tmd.sqlType match {
case java.sql.Types.BOOLEAN => "YESNO"
case java.sql.Types.BLOB => "LONGBINARY"
case java.sql.Types.SMALLINT => "INTEGER"
case java.sql.Types.BIGINT => "LONG"
case java.sql.Types.TINYINT => "BYTE"
case _ => super.defaultSqlTypeName(tmd)
case _ => super.defaultSqlTypeName(tmd, size)
}

/* Using Auto or ForwardOnly causes a NPE in the JdbcOdbcDriver */
@@ -186,7 +186,7 @@ trait AccessDriver extends JdbcDriver { driver =>
case Library.IfNull(l, r) => b"iif(isnull($l),$r,$l)"
case Library.Cast(ch @ _*) =>
(if(ch.length == 2) ch(1).asInstanceOf[LiteralNode].value.asInstanceOf[String]
else jdbcTypeFor(c.nodeType).sqlTypeName
else jdbcTypeFor(c.nodeType).sqlTypeName(None)
).toLowerCase match {
case "boolean" => b"cbool(${ch(0)})"
case "double" => b"cdbl(${ch(0)})"
@@ -108,11 +108,11 @@ trait DerbyDriver extends JdbcDriver { driver =>
override def createColumnDDLBuilder(column: FieldSymbol, table: Table[_]): ColumnDDLBuilder = new ColumnDDLBuilder(column)
override def createSequenceDDLBuilder(seq: Sequence[_]): SequenceDDLBuilder[_] = new SequenceDDLBuilder(seq)

override def defaultSqlTypeName(tmd: JdbcType[_]): String = tmd.sqlType match {
override def defaultSqlTypeName(tmd: JdbcType[_], size: Option[RelationalProfile.ColumnOption.Length]): String = tmd.sqlType match {
case java.sql.Types.BOOLEAN => "SMALLINT"
/* Derby does not have a TINYINT type, so we use SMALLINT instead. */
case java.sql.Types.TINYINT => "SMALLINT"
case _ => super.defaultSqlTypeName(tmd)
case _ => super.defaultSqlTypeName(tmd, size)
}

override val scalarFrom = Some("sysibm.sysdummy1")
@@ -127,8 +127,8 @@ trait DerbyDriver extends JdbcDriver { driver =>
val (toVarchar, tn) = {
val tn =
(if(ch.length == 2) ch(1).asInstanceOf[LiteralNode].value.asInstanceOf[String]
else jdbcTypeFor(c.nodeType).sqlTypeName).toLowerCase
if(tn == "varchar") (true, columnTypes.stringJdbcType.sqlTypeName)
else jdbcTypeFor(c.nodeType).sqlTypeName(None)).toLowerCase
if(tn == "varchar") (true, columnTypes.stringJdbcType.sqlTypeName(None))
else if(tn.startsWith("varchar")) (true, tn)
else (false, tn)
}
@@ -138,13 +138,13 @@ trait DerbyDriver extends JdbcDriver { driver =>
case Library.IfNull(l, r) =>
/* Derby does not support IFNULL so we use COALESCE instead,
* and it requires NULLs to be casted to a suitable type */
b"coalesce(cast($l as ${jdbcTypeFor(c.nodeType).sqlTypeName}),!$r)"
b"coalesce(cast($l as ${jdbcTypeFor(c.nodeType).sqlTypeName(None)}),!$r)"
case Library.SilentCast(LiteralNode(None)) :@ JdbcType(ti, _) if currentPart == SelectPart =>
// Cast NULL to the correct type
b"cast(null as ${ti.sqlTypeName})"
b"cast(null as ${ti.sqlTypeName(None)})"
case LiteralNode(None) :@ JdbcType(ti, _) if currentPart == SelectPart =>
// Cast NULL to the correct type
b"cast(null as ${ti.sqlTypeName})"
b"cast(null as ${ti.sqlTypeName(None)})"
case (c @ LiteralNode(v)) :@ JdbcType(ti, option) if currentPart == SelectPart =>
/* The Derby embedded driver has a bug (DERBY-4671) which results in a
* NullPointerException when using bind variables in a SELECT clause.
@@ -153,7 +153,7 @@ trait DerbyDriver extends JdbcDriver { driver =>
if(c.volatileHint || !ti.hasLiteralForm) {
b"cast("
b +?= { (p, idx, param) => if(option) ti.setOption(v.asInstanceOf[Option[Any]], p, idx) else ti.setValue(v, p, idx) }
b" as ${ti.sqlTypeName})"
b" as ${ti.sqlTypeName(None)})"
} else super.expr(c, skipParens)
case Library.NextValue(SequenceNode(name)) => b"(next value for `$name)"
case Library.CurrentValue(_*) => throw new SlickException("Derby does not support CURRVAL")
@@ -218,7 +218,7 @@ trait DerbyDriver extends JdbcDriver { driver =>

class UUIDJdbcType extends super.UUIDJdbcType {
override def sqlType = java.sql.Types.BINARY
override def sqlTypeName = "CHAR(16) FOR BIT DATA"
override def sqlTypeName(size: Option[RelationalProfile.ColumnOption.Length]) = "CHAR(16) FOR BIT DATA"
}
}
}
@@ -61,9 +61,10 @@ trait H2Driver extends JdbcDriver { driver =>
override def createUpsertBuilder(node: Insert): InsertBuilder = new UpsertBuilder(node)
override def createCountingInsertInvoker[U](compiled: CompiledInsert) = new CountingInsertInvoker[U](compiled)

override def defaultSqlTypeName(tmd: JdbcType[_]): String = tmd.sqlType match {
case java.sql.Types.VARCHAR => "VARCHAR"
case _ => super.defaultSqlTypeName(tmd)
override def defaultSqlTypeName(tmd: JdbcType[_], size: Option[RelationalProfile.ColumnOption.Length]): String = tmd.sqlType match {
case java.sql.Types.VARCHAR =>
size.fold("VARCHAR")(l => if(l.varying) s"VARCHAR(${l.length})" else s"CHAR(${l.length})")
case _ => super.defaultSqlTypeName(tmd, size)
}

class QueryBuilder(tree: Node, state: CompilerState) extends super.QueryBuilder(tree, state) with OracleStyleRowNum {
@@ -7,7 +7,7 @@ import scala.slick.dbio._
import scala.slick.lifted._
import scala.slick.ast._
import scala.slick.util.MacroSupport.macroSupportInterpolation
import scala.slick.profile.{SqlProfile, Capability}
import scala.slick.profile.{SqlProfile, Capability, RelationalProfile}
import scala.slick.compiler.{Phase, CompilerState}
import scala.slick.model.Model
import scala.slick.jdbc.meta.MTable
@@ -92,11 +92,11 @@ trait HsqldbDriver extends JdbcDriver { driver =>

class JdbcTypes extends super.JdbcTypes {
override val byteArrayJdbcType = new ByteArrayJdbcType {
override val sqlTypeName = "LONGVARBINARY"
override def sqlTypeName(size: Option[RelationalProfile.ColumnOption.Length]) = "LONGVARBINARY"
}
override val uuidJdbcType = new UUIDJdbcType {
override def sqlType = java.sql.Types.BINARY
override def sqlTypeName = "BINARY(16)"
override def sqlTypeName(size: Option[RelationalProfile.ColumnOption.Length]) = "BINARY(16)"
}
}

@@ -330,7 +330,7 @@ trait JdbcStatementBuilderComponent { driver: JdbcDriver =>
case Library.Cast(ch @ _*) =>
val tn =
if(ch.length == 2) ch(1).asInstanceOf[LiteralNode].value.asInstanceOf[String]
else jdbcTypeFor(n.nodeType).sqlTypeName
else jdbcTypeFor(n.nodeType).sqlTypeName(None)
if(supportsCast) b"cast(${ch(0)} as $tn)"
else b"{fn convert(!${ch(0)},$tn)}"
case Library.SilentCast(ch) => b"$ch"
@@ -705,18 +705,11 @@ trait JdbcStatementBuilderComponent { driver: JdbcDriver =>
init()

protected def init() {
if(
column.options.collect{
case _: SqlProfile.ColumnOption.SqlType =>
case _: RelationalProfile.ColumnOption.Length[_] =>
}.size > 1
){
throw new SlickException("Please specify either ColumnOption DBType or Length, not both for column ${column.name}.")
}

for(o <- column.options) handleColumnOption(o)
if(sqlType eq null) sqlType = jdbcType.sqlTypeName
else customSqlType = true
if(sqlType ne null) {
size.foreach(l => sqlType += s"($l)")
customSqlType = true
} else sqlType = jdbcType.sqlTypeName(size.map(l => RelationalProfile.ColumnOption.Length(l, varying)))
}

protected def handleColumnOption(o: ColumnOption[_]): Unit = o match {
@@ -731,19 +724,7 @@ trait JdbcStatementBuilderComponent { driver: JdbcDriver =>
case RelationalProfile.ColumnOption.Default(v) => defaultLiteral = valueToSQLLiteral(v, column.tpe)
}

def appendType(sb: StringBuilder): Unit = {
if(size == None){
sb append sqlType
}
size.foreach{ s =>
// TODO: this probably needs to be generalized and unified with defaultSqlTypeName
if(varying)
sb append "VARCHAR"
else
sb append "CHAR"
sb append "("+s+")"
}
}
def appendType(sb: StringBuilder): Unit = sb append sqlType

def appendColumn(sb: StringBuilder) {
sb append quoteIdentifier(column.name) append ' '
@@ -5,7 +5,7 @@ import java.util.UUID
import scala.slick.SlickException
import scala.slick.ast._
import scala.slick.jdbc.JdbcType
import scala.slick.profile.RelationalTypesComponent
import scala.slick.profile.{RelationalProfile, RelationalTypesComponent}
import scala.reflect.ClassTag

trait JdbcTypesComponent extends RelationalTypesComponent { driver: JdbcDriver =>
@@ -15,12 +15,12 @@ trait JdbcTypesComponent extends RelationalTypesComponent { driver: JdbcDriver =
def comap(u: U): T

def newSqlType: Option[Int] = None
def newSqlTypeName: Option[String] = None
def newSqlTypeName(size: Option[RelationalProfile.ColumnOption.Length]): Option[String] = None
def newValueToSQLLiteral(value: T): Option[String] = None
def newHasLiteralForm: Option[Boolean] = None

def sqlType = newSqlType.getOrElse(tmd.sqlType)
def sqlTypeName = newSqlTypeName.getOrElse(tmd.sqlTypeName)
def sqlTypeName(size: Option[RelationalProfile.ColumnOption.Length]) = newSqlTypeName(size).getOrElse(tmd.sqlTypeName(size))
def setValue(v: T, p: PreparedStatement, idx: Int) = tmd.setValue(map(v), p, idx)
def setNull(p: PreparedStatement, idx: Int): Unit = tmd.setNull(p, idx)
def getValue(r: ResultSet, idx: Int) = {
@@ -73,19 +73,20 @@ trait JdbcTypesComponent extends RelationalTypesComponent { driver: JdbcDriver =
case t => throw new SlickException("JdbcProfile has no JdbcType for type "+t)
}): JdbcType[_]).asInstanceOf[JdbcType[Any]]

def defaultSqlTypeName(tmd: JdbcType[_]): String = tmd.sqlType match {
case java.sql.Types.VARCHAR => "VARCHAR(254)"
def defaultSqlTypeName(tmd: JdbcType[_], size: Option[RelationalProfile.ColumnOption.Length]): String = tmd.sqlType match {
case java.sql.Types.VARCHAR =>
size.fold("VARCHAR(254)")(l => if(l.varying) s"VARCHAR(${l.length})" else s"CHAR(${l.length})")
case java.sql.Types.DECIMAL => "DECIMAL(21,2)"
case t => JdbcTypesComponent.typeNames.getOrElse(t,
throw new SlickException("No SQL type name found in java.sql.Types for code "+t))
}

abstract class DriverJdbcType[@specialized T](implicit val classTag: ClassTag[T]) extends JdbcType[T] {
def scalaType = ScalaBaseType[T]
def sqlTypeName: String = driver.defaultSqlTypeName(this)
def sqlTypeName(size: Option[RelationalProfile.ColumnOption.Length]): String = driver.defaultSqlTypeName(this, size)
def valueToSQLLiteral(value: T) =
if(hasLiteralForm) value.toString
else throw new SlickException(sqlTypeName + " does not have a literal representation")
else throw new SlickException(sqlTypeName(None) + " does not have a literal representation")
def hasLiteralForm = true
def wasNull(r: ResultSet, idx: Int) = r.wasNull()
def setNull(p: PreparedStatement, idx: Int): Unit = p.setNull(idx, sqlType)
@@ -151,7 +152,7 @@ trait JdbcTypesComponent extends RelationalTypesComponent { driver: JdbcDriver =

class CharJdbcType extends DriverJdbcType[Char] {
def sqlType = java.sql.Types.CHAR
override def sqlTypeName = "CHAR(1)"
override def sqlTypeName(size: Option[RelationalProfile.ColumnOption.Length]) = "CHAR(1)"
def setValue(v: Char, p: PreparedStatement, idx: Int) = stringJdbcType.setValue(String.valueOf(v), p, idx)
def getValue(r: ResultSet, idx: Int) = {
val s = stringJdbcType.getValue(r, idx)

0 comments on commit cc8d3f0

Please sign in to comment.
You can’t perform that action at this time.