diff --git a/src/main/scala/fr/loicknuchel/safeql/Cond.scala b/src/main/scala/fr/loicknuchel/safeql/Cond.scala index b150b7d..123edf0 100644 --- a/src/main/scala/fr/loicknuchel/safeql/Cond.scala +++ b/src/main/scala/fr/loicknuchel/safeql/Cond.scala @@ -30,11 +30,11 @@ object Cond { override def fr: Fragment = f.fr ++ fr0"=$value" } - final case class IsField[A](f1: Field[A], f2: Field[A]) extends Cond(List(f1, f2)) { - override def fr: Fragment = f1.fr ++ fr0"=" ++ f2.fr + final case class IsNotValue[A: Put](f: Field[A], value: A) extends Cond(List(f)) { + override def fr: Fragment = f.fr ++ fr0" != $value" } - final case class IsFieldLeftOpt[A](f1: Field[Option[A]], f2: Field[A]) extends Cond(List(f1, f2)) { + final case class IsField[A](f1: Field[A], f2: Field[A]) extends Cond(List(f1, f2)) { override def fr: Fragment = f1.fr ++ fr0"=" ++ f2.fr } @@ -42,24 +42,20 @@ object Cond { override def fr: Fragment = f.fr ++ fr0"=(" ++ s.fr ++ fr0")" } - final case class IsNotValue[A: Put](f: Field[A], value: A) extends Cond(List(f)) { - override def fr: Fragment = f.fr ++ fr0" != $value" - } - final case class Like[A](f: Field[A], value: String) extends Cond(List(f)) { override def fr: Fragment = f.fr ++ fr0" LIKE $value" } - final case class ILike[A](f: Field[A], value: String) extends Cond(List(f)) { - override def fr: Fragment = f.fr ++ fr0" ILIKE $value" + final case class NotLike[A](f: Field[A], value: String) extends Cond(List(f)) { + override def fr: Fragment = f.fr ++ fr0" NOT LIKE $value" } final case class LikeExpr(e: Expr, value: String) extends Cond(e.getFields) { override def fr: Fragment = e.fr ++ fr0" LIKE $value" } - final case class NotLike[A](f: Field[A], value: String) extends Cond(List(f)) { - override def fr: Fragment = f.fr ++ fr0" NOT LIKE $value" + final case class ILike[A](f: Field[A], value: String) extends Cond(List(f)) { + override def fr: Fragment = f.fr ++ fr0" ILIKE $value" } final case class GtValue[A: Put](f: Field[A], value: A) extends Cond(List(f)) { diff --git a/src/main/scala/fr/loicknuchel/safeql/Field.scala b/src/main/scala/fr/loicknuchel/safeql/Field.scala index 739c301..8c8756e 100644 --- a/src/main/scala/fr/loicknuchel/safeql/Field.scala +++ b/src/main/scala/fr/loicknuchel/safeql/Field.scala @@ -24,19 +24,19 @@ sealed trait Field[A] { def is(value: A)(implicit p: Put[A]): Cond = IsValue(this, value) + def isNot(value: A)(implicit p: Put[A]): Cond = IsNotValue(this, value) + def is(field: Field[A]): Cond = IsField(this, field) def is(select: Query.Select[A]): Cond = IsQuery(this, select) - def isNot(value: A)(implicit p: Put[A]): Cond = IsNotValue(this, value) - // TODO restrict to fields with sql string type def like(value: String): Cond = Like(this, value) - def ilike(value: String): Cond = ILike(this, value) - def notLike(value: String): Cond = NotLike(this, value) + def ilike(value: String): Cond = ILike(this, value) + def gt(value: A)(implicit p: Put[A]): Cond = GtValue(this, value) def gte(value: A)(implicit p: Put[A]): Cond = GteValue(this, value) @@ -91,45 +91,30 @@ object Field { } -class SqlField[A, +T <: Table.SqlTable](val table: T, - val name: String, - val info: SqlField.JdbcInfo, - val alias: Option[String]) extends Field[A] { +sealed trait SqlField[A, +T <: Table.SqlTable] extends Field[A] { + val table: T + val name: String + val info: SqlField.JdbcInfo + val alias: Option[String] + override def ref: Fragment = const0(s"${table.getAlias.getOrElse(table.getName)}.$name") override def value: Fragment = ref - def nullable: Boolean = info.nullable - - def as(alias: String): SqlField[A, T] = new SqlField[A, T](table, name, info, Some(alias)) + override def as(alias: String): SqlField[A, T] // create a null TableField based on a sql field, useful on union when a field is available on one side only def asNull: NullField[A] = NullField[A](alias.getOrElse(name)) def asNull(name: String): NullField[A] = NullField[A](name) - override def toString: String = s"SqlField(${table.getName}.$name)" - - def canEqual(other: Any): Boolean = other.isInstanceOf[SqlField[_, _]] - - override def equals(other: Any): Boolean = other match { - case that: SqlField[_, _] => - (that canEqual this) && - table == that.table && - name == that.name - case _ => false - } - - override def hashCode(): Int = { - val state: List[Object] = List(table, name) - state.map(_.hashCode()).foldLeft(0)((a, b) => 31 * a + b) - } + def nullable: Boolean = info.nullable } object SqlField { - def apply[A, T <: Table.SqlTable](table: T, name: String, jdbcDeclaration: String, jdbcType: JdbcType, nullable: Boolean, index: Int): SqlField[A, T] = - new SqlField(table, name, JdbcInfo(nullable, index, jdbcType, jdbcDeclaration), None) + def apply[A, T <: Table.SqlTable](table: T, name: String, jdbcDeclaration: String, jdbcType: JdbcType, nullable: Boolean, index: Int): SqlFieldRaw[A, T] = + SqlFieldRaw(table, name, JdbcInfo(nullable, index, jdbcType, jdbcDeclaration), None) def apply[A, T <: Table.SqlTable, T2 <: Table.SqlTable](table: T, name: String, jdbcDeclaration: String, jdbcType: JdbcType, nullable: Boolean, index: Int, references: SqlField[A, T2]): SqlFieldRef[A, T, T2] = SqlFieldRef(table, name, JdbcInfo(nullable, index, jdbcType, jdbcDeclaration), None, references) @@ -138,11 +123,22 @@ object SqlField { } -case class SqlFieldRef[A, T <: Table.SqlTable, T2 <: Table.SqlTable](override val table: T, - override val name: String, - override val info: SqlField.JdbcInfo, - override val alias: Option[String], - references: SqlField[A, T2]) extends SqlField[A, T](table, name, info, alias) { +case class SqlFieldRaw[A, +T <: Table.SqlTable](table: T, + name: String, + info: SqlField.JdbcInfo, + alias: Option[String]) extends SqlField[A, T] { + override def as(alias: String): SqlFieldRaw[A, T] = copy(alias = Some(alias)) + + override def toString: String = s"SqlFieldRaw(${table.getName}.$name)" +} + +case class SqlFieldRef[A, T <: Table.SqlTable, T2 <: Table.SqlTable](table: T, + name: String, + info: SqlField.JdbcInfo, + alias: Option[String], + references: SqlField[A, T2]) extends SqlField[A, T] { + override def as(alias: String): SqlFieldRef[A, T, T2] = copy(alias = Some(alias)) + override def toString: String = s"SqlFieldRef(${table.getName}.$name, ${references.table.getName}.${references.name})" } @@ -183,10 +179,12 @@ object AggField { def apply[A](name: String, alias: String): SimpleAggField[A] = SimpleAggField(name, Some(alias)) + def apply[A](query: Query[A]): QueryAggField[A] = QueryAggField(query, None) + def apply[A](query: Query[A], alias: String): QueryAggField[A] = QueryAggField(query, Some(alias)) } -case class SimpleAggField[A](name: String, alias: Option[String]) extends AggField[A] { +case class SimpleAggField[A](name: String, alias: Option[String] = None) extends AggField[A] { override def ref: Fragment = const0(alias.getOrElse(name)) override def value: Fragment = const0(name) @@ -194,7 +192,7 @@ case class SimpleAggField[A](name: String, alias: Option[String]) extends AggFie override def as(alias: String): AggField[A] = copy(alias = Some(alias)) } -case class QueryAggField[A](query: Query[A], alias: Option[String]) extends AggField[A] { +case class QueryAggField[A](query: Query[A], alias: Option[String] = None) extends AggField[A] { override val name: String = "(" + query.sql + ")" override def ref: Fragment = alias.map(const0(_)).getOrElse(query.fr) diff --git a/src/main/scala/fr/loicknuchel/safeql/Query.scala b/src/main/scala/fr/loicknuchel/safeql/Query.scala index 2606077..86c0874 100644 --- a/src/main/scala/fr/loicknuchel/safeql/Query.scala +++ b/src/main/scala/fr/loicknuchel/safeql/Query.scala @@ -42,6 +42,8 @@ object Query { case class Builder[T <: Table.SqlTable](private val table: T, private val fields: List[SqlField[_, T]]) { def fields(fields: List[SqlField[_, T]]): Builder[T] = copy(fields = fields) + def fields(fields: SqlField[_, T]*): Builder[T] = this.fields(fields.toList) + // FIXME 2020-10-15: temporary hack waiting I solve the Put[Option[A]] problem (cf https://gist.github.com/loicknuchel/2297d612b58b399395bdd08d3c6dd217) def values(fr: Fragment): Insert[T] = Insert(table, fields, fr) @@ -135,8 +137,6 @@ object Query { if (f.nullable) copy(values = values :+ (const0(f.name) ++ fr0"=$value")) else throw new Exception(s"Can't use an Option for non nullable field $f") } - def all: Update[T] = Update(table, values, WhereClause(None, None, None)) - def where(cond: Cond): Update[T] = Exceptions.check(cond, table, Update(table, values, WhereClause(Some(cond), None, None))) def where(cond: T => Cond): Update[T] = where(cond(table)) @@ -157,8 +157,6 @@ object Query { object Delete { case class Builder[T <: Table.SqlTable](private val table: T) { - def all: Delete[T] = Delete(table, WhereClause(None, None, None)) - def where(cond: Cond): Delete[T] = Exceptions.check(cond, table, Delete(table, WhereClause(Some(cond), None, None))) def where(cond: T => Cond): Delete[T] = where(cond(table)) @@ -256,6 +254,7 @@ object Query { def withoutFields(fns: (T => Field[_])*): Builder[T] = dropFields(fns.map(f => f(table)).toList) + // unsafe option is useful when a nested queries use a parent field, there is no way to track this right now as it's built independently def where(cond: Cond, unsafe: Boolean = false): Builder[T] = if (unsafe) copy(where = WhereClause(Some(cond), None, None)) else Exceptions.check(cond, table, copy(where = WhereClause(Some(cond), None, None))) @@ -277,7 +276,7 @@ object Query { def union[T2 <: Table](other: Builder[T2], alias: Option[String] = None, sorts: List[(String, String, List[String])] = List(), search: List[String] = List()): Table.UnionTable = { if (fields.length != other.fields.length) throw new Exception(s"Field number do not match (${fields.length} vs ${other.fields.length})") - val invalidFields = fields.zip(other.fields).filter { case (f1, f2) => f1.alias.getOrElse(f1.name) != f2.alias.getOrElse(f2.name) } // TODO check also match of sql type (should be added) + val invalidFields = fields.zip(other.fields).filter { case (f1, f2) => f1.alias.getOrElse(f1.name) != f2.alias.getOrElse(f2.name) } // FIXME check also match of sql type (should be added) if (invalidFields.nonEmpty) throw new Exception(s"Some fields do not match: ${invalidFields.map { case (f1, f2) => f1.name + " != " + f2.name }.mkString(", ")}") val getFields = fields.map(f => TableField(f.alias.getOrElse(f.name), alias)) diff --git a/src/main/scala/fr/loicknuchel/safeql/gen/Generator.scala b/src/main/scala/fr/loicknuchel/safeql/gen/Generator.scala index 30462c3..c58e1ca 100644 --- a/src/main/scala/fr/loicknuchel/safeql/gen/Generator.scala +++ b/src/main/scala/fr/loicknuchel/safeql/gen/Generator.scala @@ -29,47 +29,47 @@ object Generator { .dataSource(new DriverDataSource(this.getClass.getClassLoader, reader.driver, reader.url, reader.user, reader.pass)) .locations(flywayLocations: _*) .load() - new FlywayGeneratorBuilder(flyway, reader) + FlywayGeneratorBuilder(flyway, reader) } - class FlywayGeneratorBuilder(flyway: Flyway, reader: H2Reader) { - def writer(writer: Writer): FlywayGenerator = new FlywayGenerator(flyway, reader, writer) + case class FlywayGeneratorBuilder(flyway: Flyway, reader: H2Reader) { + def writer(writer: Writer): FlywayGenerator = FlywayGenerator(flyway, reader, writer) - def excludes(regex: String): FlywayGeneratorBuilder = new FlywayGeneratorBuilder(flyway, reader.excludes(regex)) + def excludes(regex: String): FlywayGeneratorBuilder = FlywayGeneratorBuilder(flyway, reader.excludes(regex)) } - class FlywayGenerator(flyway: Flyway, reader: H2Reader, writer: Writer) { + case class FlywayGenerator(flyway: Flyway, reader: H2Reader, writer: Writer) { def generate(): IO[Unit] = IO(flyway.migrate()).flatMap(_ => Generator.generate(reader, writer)) - def excludes(regex: String): FlywayGenerator = new FlywayGenerator(flyway, reader.excludes(regex), writer) + def excludes(regex: String): FlywayGenerator = FlywayGenerator(flyway, reader.excludes(regex), writer) } /** * SQL files Generator */ - def fromFiles(paths: List[String]): SQLFilesGeneratorBuilder = { + def sqlFiles(paths: List[String]): SQLFilesGeneratorBuilder = { val reader = H2Reader( url = s"jdbc:h2:mem:${UUID.randomUUID()};MODE=PostgreSQL;DATABASE_TO_UPPER=false;DB_CLOSE_DELAY=-1", schema = Some("PUBLIC"), excludes = None) - new SQLFilesGeneratorBuilder(paths, reader) + SQLFilesGeneratorBuilder(paths, reader) } - class SQLFilesGeneratorBuilder(paths: List[String], reader: H2Reader) { - def writer(writer: Writer): SQLFilesGenerator = new SQLFilesGenerator(paths, reader, writer) + case class SQLFilesGeneratorBuilder(paths: List[String], reader: H2Reader) { + def writer(writer: Writer): SQLFilesGenerator = SQLFilesGenerator(paths, reader, writer) - def excludes(regex: String): SQLFilesGeneratorBuilder = new SQLFilesGeneratorBuilder(paths, reader.excludes(regex)) + def excludes(regex: String): SQLFilesGeneratorBuilder = SQLFilesGeneratorBuilder(paths, reader.excludes(regex)) } - class SQLFilesGenerator(paths: List[String], reader: H2Reader, writer: Writer) { + case class SQLFilesGenerator(paths: List[String], reader: H2Reader, writer: Writer) { def generate(): IO[Unit] = for { files <- paths.map(FileUtils.read).sequence.toIO _ <- files.map(exec(_, reader.xa)).sequence _ <- Generator.generate(reader, writer) } yield () - def excludes(regex: String): SQLFilesGenerator = new SQLFilesGenerator(paths, reader.excludes(regex), writer) + def excludes(regex: String): SQLFilesGenerator = SQLFilesGenerator(paths, reader.excludes(regex), writer) private def exec(script: String, xa: doobie.Transactor[IO]): IO[Int] = Update0(script, None).run.transact(xa).recoverWith { case NonFatal(e) => IO.raiseError(FailedScript(script, e)) } @@ -79,13 +79,13 @@ object Generator { * Reader Generator */ - def reader(reader: Reader) = new ReaderGeneratorBuilder(reader) + def reader(reader: Reader): ReaderGeneratorBuilder = ReaderGeneratorBuilder(reader) - class ReaderGeneratorBuilder(reader: Reader) { - def writer(writer: Writer): ReaderGenerator = new ReaderGenerator(reader, writer) + case class ReaderGeneratorBuilder(reader: Reader) { + def writer(writer: Writer): ReaderGenerator = ReaderGenerator(reader, writer) } - class ReaderGenerator(reader: Reader, writer: Writer) { + case class ReaderGenerator(reader: Reader, writer: Writer) { def generate(): IO[Unit] = Generator.generate(reader, writer) } @@ -98,12 +98,12 @@ object Generator { * Allow to start with writer */ - def writer(writer: Writer) = new Builder(writer) + def writer(writer: Writer): Builder = Builder(writer) - class Builder(writer: Writer) { + case class Builder(writer: Writer) { def flyway(flywayLocations: String*): FlywayGenerator = Generator.flyway(flywayLocations: _*).writer(writer) - def fromFiles(paths: List[String]): SQLFilesGenerator = Generator.fromFiles(paths).writer(writer) + def sqlFiles(paths: List[String]): SQLFilesGenerator = Generator.sqlFiles(paths).writer(writer) def reader(reader: Reader): ReaderGenerator = Generator.reader(reader).writer(writer) } diff --git a/src/main/scala/fr/loicknuchel/safeql/gen/reader/H2Reader.scala b/src/main/scala/fr/loicknuchel/safeql/gen/reader/H2Reader.scala index ad5e4db..4a3391b 100644 --- a/src/main/scala/fr/loicknuchel/safeql/gen/reader/H2Reader.scala +++ b/src/main/scala/fr/loicknuchel/safeql/gen/reader/H2Reader.scala @@ -9,11 +9,11 @@ import fr.loicknuchel.safeql.gen.reader.H2Reader._ import scala.concurrent.ExecutionContext -class H2Reader(val url: String, - val user: String, - val pass: String, - val schema: Option[String], - val excludes: Option[String]) extends Reader { +case class H2Reader(url: String, + user: String, + pass: String, + schema: Option[String], + excludes: Option[String]) extends Reader { val driver: String = "org.h2.Driver" protected[gen] lazy val xa: doobie.Transactor[IO] = { implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global) diff --git a/src/main/scala/fr/loicknuchel/safeql/gen/writer/ScalaWriter.scala b/src/main/scala/fr/loicknuchel/safeql/gen/writer/ScalaWriter.scala index 9784dbe..a53a80c 100644 --- a/src/main/scala/fr/loicknuchel/safeql/gen/writer/ScalaWriter.scala +++ b/src/main/scala/fr/loicknuchel/safeql/gen/writer/ScalaWriter.scala @@ -8,10 +8,10 @@ import fr.loicknuchel.safeql.gen.writer.ScalaWriter.{DatabaseConfig, TableConfig import fr.loicknuchel.safeql.gen.writer.Writer.IdentifierStrategy import fr.loicknuchel.safeql.utils.StringUtils -class ScalaWriter(val directory: String, - val packageName: String, - val identifierStrategy: IdentifierStrategy, - val config: DatabaseConfig) extends Writer { +case class ScalaWriter(directory: String, + packageName: String, + identifierStrategy: IdentifierStrategy, + config: DatabaseConfig) extends Writer { require(config.getConfigErrors.isEmpty, s"DatabaseConfig has some errors :${config.getConfigErrors.map("\n - " + _).mkString}") require(StringUtils.isScalaPackage(packageName), s"'$packageName' is an invalid scala package name") @@ -125,7 +125,7 @@ class ScalaWriter(val directory: String, val fieldRef = (if (r.schema == f.schema && r.table == f.table) "" else s"${idf(r.table)}.table.") + idf(r.field) s"val $fieldName: SqlFieldRef[$valueType, $tableName, ${idf(r.table)}] = SqlField(this, ${str(f.name)}, ${str(f.jdbcTypeDeclaration)}, JdbcType.$jdbcType, nullable = ${f.nullable}, ${f.index}, $fieldRef)" }.getOrElse { - s"val $fieldName: SqlField[$valueType, $tableName] = SqlField(this, ${str(f.name)}, ${str(f.jdbcTypeDeclaration)}, JdbcType.$jdbcType, nullable = ${f.nullable}, ${f.index})" + s"val $fieldName: SqlFieldRaw[$valueType, $tableName] = SqlField(this, ${str(f.name)}, ${str(f.jdbcTypeDeclaration)}, JdbcType.$jdbcType, nullable = ${f.nullable}, ${f.index})" } } @@ -174,7 +174,7 @@ class ScalaWriter(val directory: String, object ScalaWriter { def apply(directory: String = "src/main/scala", packageName: String = "safeql", - identifierStrategy: IdentifierStrategy = Writer.IdentifierStrategy.upperCase, + identifierStrategy: IdentifierStrategy = Writer.IdentifierStrategy.UpperCase, config: DatabaseConfig = DatabaseConfig()): ScalaWriter = new ScalaWriter(directory, packageName, identifierStrategy, config) @@ -242,15 +242,15 @@ object ScalaWriter { def apply(alias: String, sort: TableConfig.Sort, search: List[String]): TableConfig = new TableConfig(Some(alias), List(sort), search) - def apply(alias: String, fields: Map[String, FieldConfig]): TableConfig = new TableConfig(Some(alias), List(), List(), fields) + def apply(alias: String, sort: TableConfig.Sort, search: List[String], fields: Map[String, FieldConfig]): TableConfig = new TableConfig(Some(alias), List(sort), search, fields) + + def apply(alias: String, sort: TableConfig.Sort, fields: Map[String, FieldConfig]): TableConfig = new TableConfig(Some(alias), List(sort), List(), fields) def apply(alias: String, sort: String, fields: Map[String, FieldConfig]): TableConfig = new TableConfig(Some(alias), List(TableConfig.Sort(sort)), List(), fields) def apply(alias: String, sort: String, search: List[String], fields: Map[String, FieldConfig]): TableConfig = new TableConfig(Some(alias), List(TableConfig.Sort(sort)), search, fields) - def apply(alias: String, sort: TableConfig.Sort, fields: Map[String, FieldConfig]): TableConfig = new TableConfig(Some(alias), List(sort), List(), fields) - - def apply(alias: String, sort: TableConfig.Sort, search: List[String], fields: Map[String, FieldConfig]): TableConfig = new TableConfig(Some(alias), List(sort), search, fields) + def apply(alias: String, fields: Map[String, FieldConfig]): TableConfig = new TableConfig(Some(alias), List(), List(), fields) case class Sort(slug: String, label: String, fields: NonEmptyList[Sort.Field]) diff --git a/src/main/scala/fr/loicknuchel/safeql/gen/writer/Writer.scala b/src/main/scala/fr/loicknuchel/safeql/gen/writer/Writer.scala index 31c6799..39cabba 100644 --- a/src/main/scala/fr/loicknuchel/safeql/gen/writer/Writer.scala +++ b/src/main/scala/fr/loicknuchel/safeql/gen/writer/Writer.scala @@ -51,21 +51,16 @@ object Writer { } object IdentifierStrategy { + private[writer] val scalaKeywords = Set("val", "var", "def", "type", "class", "object", "import", "package") - class KeepNames extends IdentifierStrategy { - // only avoid scala keywords - override def format(value: String): String = value match { - case "type" => "`type`" - case v => v - } + case object KeepNames extends IdentifierStrategy { + override def format(value: String): String = if (scalaKeywords.contains(value)) s"`$value`" else value } - class UpperCase extends IdentifierStrategy { + case object UpperCase extends IdentifierStrategy { override def format(value: String): String = value.toUpperCase } - val keepNames = new KeepNames - val upperCase = new UpperCase } } diff --git a/src/main/scala/fr/loicknuchel/safeql/utils/FileUtils.scala b/src/main/scala/fr/loicknuchel/safeql/utils/FileUtils.scala index cfa8a5a..bdf15e1 100644 --- a/src/main/scala/fr/loicknuchel/safeql/utils/FileUtils.scala +++ b/src/main/scala/fr/loicknuchel/safeql/utils/FileUtils.scala @@ -5,7 +5,6 @@ import java.nio.file.{Files, Paths} import fr.loicknuchel.safeql.utils.Extensions._ -import scala.jdk.CollectionConverters._ import scala.util.Try private[safeql] object FileUtils { @@ -32,7 +31,7 @@ private[safeql] object FileUtils { } def read(path: String): Try[String] = - Try(Files.readAllLines(Paths.get(path))).map(_.asScala.mkString("\n")) + Try(Files.readAllBytes(Paths.get(path))).map(new String(_)) def write(path: String, content: String): Try[Unit] = mkdirs(parent(path)).flatMap(_ => Try(Files.write(Paths.get(path), content.getBytes)).map(_ => ())) diff --git a/src/test/resources/sql_migrations/V1__test_schema.sql b/src/test/resources/sql_migrations/V1__test_schema.sql index 9f987fc..a1ae656 100644 --- a/src/test/resources/sql_migrations/V1__test_schema.sql +++ b/src/test/resources/sql_migrations/V1__test_schema.sql @@ -52,10 +52,11 @@ CREATE TABLE kinds date DATE, boolean BOOLEAN, int INT, + smallint SMALLINT, bigint BIGINT, double DOUBLE PRECISION, a_long_name INT ); -INSERT INTO kinds (char, varchar, timestamp, date, boolean, int, bigint, double, a_long_name) -VALUES ('char', 'varchar', TIMESTAMP '2020-08-05 10:20:00', DATE '2020-08-05', TRUE, 1, 10, 4.5, 0); +INSERT INTO kinds (char, varchar, timestamp, date, boolean, int, smallint, bigint, double, a_long_name) +VALUES ('char', 'varchar', TIMESTAMP '2020-08-05 10:20:00', DATE '2020-08-05', TRUE, 1, 4, 10, 4.5, 0); diff --git a/src/test/scala/fr/loicknuchel/safeql/CondSpec.scala b/src/test/scala/fr/loicknuchel/safeql/CondSpec.scala new file mode 100644 index 0000000..7f5e8dc --- /dev/null +++ b/src/test/scala/fr/loicknuchel/safeql/CondSpec.scala @@ -0,0 +1,59 @@ +package fr.loicknuchel.safeql + +import cats.data.NonEmptyList +import doobie.syntax.string._ +import fr.loicknuchel.safeql.testingutils.BaseSpec +import fr.loicknuchel.safeql.testingutils.database.Tables.{POSTS, USERS} +import fr.loicknuchel.safeql.testingutils.database.tables.{POSTS, USERS} + +class CondSpec extends BaseSpec { + private val sqlField: SqlFieldRaw[String, USERS] = USERS.NAME + private val expr: Expr = sqlField.lower + private val query: Query.Select.One[String] = USERS.select.fields(USERS.NAME).where(_.NAME is "lou").one[String] + private val sqlField2: SqlFieldRaw[String, POSTS] = POSTS.TITLE + private val sqlFieldRef: SqlFieldRef[String, POSTS, USERS] = POSTS.AUTHOR.asInstanceOf[SqlFieldRef[String, POSTS, USERS]] + private val tableField: TableField[String] = TableField("name") + private val nullField: NullField[String] = NullField("name") + private val queryField: QueryField[String] = QueryField(query) + private val aggField: SimpleAggField[String] = SimpleAggField("name") + private val queryAggField: QueryAggField[String] = QueryAggField(query) + + describe("Cond") { + it("should generate SQL for eq condition and each field") { + Cond.IsValue(sqlField, "lou").sql shouldBe "u.name=?" + Cond.IsValue(sqlFieldRef, "lou").sql shouldBe "p.author=?" + Cond.IsValue(tableField, "lou").sql shouldBe "name=?" + Cond.IsValue(nullField, "lou").sql shouldBe "null as name=?" // FIXME should be "name=?" + Cond.IsValue(queryField, "lou").sql shouldBe "(SELECT u.name FROM users u WHERE u.name=?)=?" + Cond.IsValue(aggField, "lou").sql shouldBe "name=?" + Cond.IsValue(queryAggField, "lou").sql shouldBe "(SELECT u.name FROM users u WHERE u.name=?)=?" + } + it("should generate SQL for sql field and each condition") { + Cond.IsValue(sqlField, "lou").sql shouldBe "u.name=?" + Cond.IsNotValue(sqlField, "lou").sql shouldBe "u.name != ?" + Cond.IsField(sqlField, sqlField2).sql shouldBe "u.name=p.title" + Cond.IsQuery(sqlField, query).sql shouldBe "u.name=(SELECT u.name FROM users u WHERE u.name=?)" + Cond.Like(sqlField, "%lou%").sql shouldBe "u.name LIKE ?" + Cond.NotLike(sqlField, "%lou%").sql shouldBe "u.name NOT LIKE ?" + Cond.LikeExpr(expr, "%lou%").sql shouldBe "LOWER(u.name) LIKE ?" + Cond.ILike(sqlField, "%lou%").sql shouldBe "u.name ILIKE ?" + Cond.GtValue(sqlField, "lou").sql shouldBe "u.name > ?" + Cond.GteValue(sqlField, "lou").sql shouldBe "u.name >= ?" + Cond.LtValue(sqlField, "lou").sql shouldBe "u.name < ?" + Cond.LteValue(sqlField, "lou").sql shouldBe "u.name <= ?" + Cond.IsNull(sqlField).sql shouldBe "u.name IS NULL" + Cond.NotNull(sqlField).sql shouldBe "u.name IS NOT NULL" + Cond.InValues(sqlField, NonEmptyList.of("lou")).sql shouldBe "u.name IN (?)" + Cond.NotInValues(sqlField, NonEmptyList.of("lou")).sql shouldBe "u.name NOT IN (?)" + Cond.InQuery(sqlField, query).sql shouldBe "u.name IN (SELECT u.name FROM users u WHERE u.name=?)" + Cond.NotInQuery(sqlField, query).sql shouldBe "u.name NOT IN (SELECT u.name FROM users u WHERE u.name=?)" + Cond.CustomCond(sqlField, fr0" LIKE 'lou'").sql shouldBe "u.name LIKE 'lou'" + + val c1 = Cond.IsValue(sqlField, "lou") + val c2 = Cond.IsValue(sqlField2, "lou") + Cond.And(c1, c2).sql shouldBe "u.name=? AND p.title=?" + Cond.Or(c1, c2).sql shouldBe "u.name=? OR p.title=?" + Cond.Parentheses(c1).sql shouldBe "(u.name=?)" + } + } +} diff --git a/src/test/scala/fr/loicknuchel/safeql/ExprSpec.scala b/src/test/scala/fr/loicknuchel/safeql/ExprSpec.scala new file mode 100644 index 0000000..f1d229d --- /dev/null +++ b/src/test/scala/fr/loicknuchel/safeql/ExprSpec.scala @@ -0,0 +1,26 @@ +package fr.loicknuchel.safeql + +import fr.loicknuchel.safeql.testingutils.BaseSpec +import fr.loicknuchel.safeql.testingutils.database.Tables.{POSTS, USERS} +import fr.loicknuchel.safeql.testingutils.database.tables.{POSTS, USERS} + +class ExprSpec extends BaseSpec { + private val sqlField: SqlFieldRaw[String, USERS] = USERS.NAME + private val sqlField2: SqlFieldRaw[String, POSTS] = POSTS.TITLE + private val query: Query.Select.One[String] = USERS.select.fields(USERS.NAME).where(_.NAME is "lou").one[String] + + describe("Expr") { + it("should generate SQL") { + Expr.Value("lou").sql shouldBe "?" + Expr.ValueField(sqlField).sql shouldBe "u.name" + Expr.Random().sql shouldBe "RANDOM()" + Expr.SubQuery(query).sql shouldBe "(SELECT u.name FROM users u WHERE u.name=?)" + + val e1 = Expr.ValueField(sqlField) + val e2 = Expr.ValueField(sqlField2) + Expr.Lower(e1).sql shouldBe "LOWER(u.name)" + Expr.Floor(e1).sql shouldBe "FLOOR(u.name)" + Expr.Times(e1, e2).sql shouldBe "u.name * p.title" + } + } +} diff --git a/src/test/scala/fr/loicknuchel/safeql/FieldSpec.scala b/src/test/scala/fr/loicknuchel/safeql/FieldSpec.scala index f109cdf..eec9762 100644 --- a/src/test/scala/fr/loicknuchel/safeql/FieldSpec.scala +++ b/src/test/scala/fr/loicknuchel/safeql/FieldSpec.scala @@ -1,28 +1,32 @@ package fr.loicknuchel.safeql +import cats.data.NonEmptyList +import doobie.syntax.string._ import fr.loicknuchel.safeql.testingutils.BaseSpec import fr.loicknuchel.safeql.testingutils.database.Tables.{POSTS, USERS} class FieldSpec extends BaseSpec { + private val field = USERS.NAME + private val field2 = POSTS.TITLE private val q = USERS.select.fields(USERS.NAME).option[String] describe("Field") { it("should generate sql for SqlField") { - val f1 = new SqlField(POSTS, "title", POSTS.TITLE.info, None) + val f1 = SqlFieldRaw(POSTS, "title", POSTS.TITLE.info, None) f1.fr.query.sql shouldBe "p.title" f1.value.query.sql shouldBe "p.title" f1.ref.query.sql shouldBe "p.title" - val f2 = new SqlField(POSTS, "title", POSTS.TITLE.info, Some("t")) + val f2 = SqlFieldRaw(POSTS, "title", POSTS.TITLE.info, Some("t")) f2.fr.query.sql shouldBe "p.title as t" f2.value.query.sql shouldBe "p.title" f2.ref.query.sql shouldBe "p.title" } it("should generate sql for SqlFieldRef") { - val f1 = new SqlFieldRef(POSTS, "author", POSTS.AUTHOR.info, None, USERS.ID) + val f1 = SqlFieldRef(POSTS, "author", POSTS.AUTHOR.info, None, USERS.ID) f1.fr.query.sql shouldBe "p.author" f1.value.query.sql shouldBe "p.author" f1.ref.query.sql shouldBe "p.author" - val f2 = new SqlFieldRef(POSTS, "author", POSTS.AUTHOR.info, Some("a"), USERS.ID) + val f2 = SqlFieldRef(POSTS, "author", POSTS.AUTHOR.info, Some("a"), USERS.ID) f2.fr.query.sql shouldBe "p.author as a" f2.value.query.sql shouldBe "p.author" f2.ref.query.sql shouldBe "p.author" @@ -81,10 +85,67 @@ class FieldSpec extends BaseSpec { f2.value.query.sql shouldBe "(SELECT u.name FROM users u)" f2.ref.query.sql shouldBe "SELECT u.name FROM users u" } + it("should build cond") { + field.is("lou") shouldBe Cond.IsValue(field, "lou") + field.isNot("lou") shouldBe Cond.IsNotValue(field, "lou") + field.is(field2) shouldBe Cond.IsField(field, field2) + field.is(q) shouldBe Cond.IsQuery(field, q) + field.like("%lou%") shouldBe Cond.Like(field, "%lou%") + field.notLike("%lou%") shouldBe Cond.NotLike(field, "%lou%") + field.ilike("%lou%") shouldBe Cond.ILike(field, "%lou%") + field.gt("lou") shouldBe Cond.GtValue(field, "lou") + field.gte("lou") shouldBe Cond.GteValue(field, "lou") + field.lt("lou") shouldBe Cond.LtValue(field, "lou") + field.lte("lou") shouldBe Cond.LteValue(field, "lou") + field.isNull shouldBe Cond.IsNull(field) + field.notNull shouldBe Cond.NotNull(field) + field.in(NonEmptyList.of("lou")) shouldBe Cond.InValues(field, NonEmptyList.of("lou")) + field.notIn(NonEmptyList.of("lou")) shouldBe Cond.NotInValues(field, NonEmptyList.of("lou")) + field.in(q) shouldBe Cond.InQuery(field, q) + field.notIn(q) shouldBe Cond.NotInQuery(field, q) + field.cond(fr0" LIKE 'lou'").sql shouldBe Cond.CustomCond(field, fr0" LIKE 'lou'").sql // as Fragments have identity equality + } + it("should build expr") { + field.lower shouldBe Expr.Lower(Expr.ValueField(field)) + } + it("should be aliased") { + USERS.NAME.alias shouldBe None + USERS.NAME.as("n").alias shouldBe Some("n") + + POSTS.AUTHOR.alias shouldBe None + POSTS.AUTHOR.as("a").alias shouldBe Some("a") + + TableField("name").as("n") shouldBe TableField("name", alias = Some("n")) + NullField("name").as("n") shouldBe NullField("n") + QueryField(q).as("n") shouldBe QueryField(q, alias = Some("n")) + SimpleAggField("name").as("n") shouldBe SimpleAggField("name", alias = Some("n")) + QueryAggField(q).as("n") shouldBe QueryAggField(q, alias = Some("n")) + } + describe("SqlField") { + it("should create null fields") { + USERS.NAME.asNull shouldBe NullField("name") + USERS.NAME.as("n").asNull shouldBe NullField("n") + USERS.NAME.asNull("n") shouldBe NullField("n") + } + } + describe("AggField") { + it("should have many constructors") { + AggField("name") shouldBe SimpleAggField("name") + AggField("name", "n") shouldBe SimpleAggField("name", Some("n")) + AggField(q) shouldBe QueryAggField(q) + AggField(q, "n") shouldBe QueryAggField(q, Some("n")) + } + } describe("Order") { + it("should have many constructors") { + Field.Order(field, asc = true) shouldBe Field.Order(field, asc = true, None) + Field.Order("id") shouldBe Field.Order(TableField("id"), asc = true, None) + Field.Order("-id") shouldBe Field.Order(TableField("id"), asc = false, None) + Field.Order("id", Some("u")) shouldBe Field.Order(TableField("id", Some("u")), asc = true, None) + } it("should build order with nulls last") { Field.Order(TableField("name"), asc = true, None).fr(nullsFirst = false).query.sql shouldBe "name IS NULL, name" - // Field.Order(TableField("null").as("name"), asc = true, None).fr(nullsFirst = false).query.sql shouldBe "name IS NULL, name" + // FIXME Field.Order(TableField("null").as("name"), asc = true, None).fr(nullsFirst = false).query.sql shouldBe "name IS NULL, name" Field.Order(TableField("name"), asc = false, None).fr(nullsFirst = false).query.sql shouldBe "name IS NULL, name DESC" } it("should build order with nulls first") { diff --git a/src/test/scala/fr/loicknuchel/safeql/QuerySpec.scala b/src/test/scala/fr/loicknuchel/safeql/QuerySpec.scala index c114f9e..c5faca3 100644 --- a/src/test/scala/fr/loicknuchel/safeql/QuerySpec.scala +++ b/src/test/scala/fr/loicknuchel/safeql/QuerySpec.scala @@ -4,6 +4,7 @@ import java.time.Instant import cats.data.NonEmptyList import doobie.util.Put +import doobie.syntax.string._ import doobie.util.meta.Meta import fr.loicknuchel.safeql.Query.Inner._ import fr.loicknuchel.safeql.models.Page @@ -33,17 +34,36 @@ class QuerySpec extends BaseSpec { USERS.insert.values(User.loic.id, User.loic.name, User.loic.email).sql shouldBe "INSERT INTO users (id, name, email) VALUES (?, ?, ?)" } + it("should support Fragment insert until a good solution is found for Optionals") { + USERS.insert.values(fr0"${User.loic.id}, ${User.loic.name}, ${User.loic.email}").sql shouldBe "INSERT INTO users (id, name, email) VALUES (?, ?, ?)" + } + it("should support partial inserts") { + USERS.insert.fields(USERS.ID, USERS.NAME).values(User.loic.id, User.loic.name).sql shouldBe "INSERT INTO users (id, name) VALUES (?, ?)" + } it("should fail on bad argument number") { an[Exception] should be thrownBy CATEGORIES.insert.values(Category.tech.id) } } describe("Update") { - + it("should update data in a table") { + USERS.update.set(_.NAME, "lou").where(_.ID is User.loic.id).sql shouldBe "UPDATE users u SET name=? WHERE u.id=?" + } + it("should handle optional values only on nullable fields") { + USERS.update.set(_.EMAIL, Some("test")).where(_.ID is User.loic.id).sql shouldBe "UPDATE users u SET email=? WHERE u.id=?" + an[Exception] should be thrownBy USERS.update.set(_.NAME, Some("test")).where(_.ID is User.loic.id).sql + } } describe("Delete") { - + it("should delete data in a table") { + USERS.delete.where(_.ID is User.loic.id).sql shouldBe "DELETE FROM users u WHERE u.id=?" + } } describe("Select") { + describe("all") { + it("should build a list query") { + USERS.select.all[User].sql shouldBe "SELECT u.id, u.name, u.email FROM users u" + } + } describe("page") { it("should build a paginated query") { USERS.select.page[User](p, ctx).sql shouldBe "SELECT u.id, u.name, u.email FROM users u LIMIT 20 OFFSET 0" @@ -113,6 +133,32 @@ class QuerySpec extends BaseSpec { } } } + describe("option") { + it("should build a list query") { + USERS.select.where(_.ID is User.loic.id).option[User].sql shouldBe "SELECT u.id, u.name, u.email FROM users u WHERE u.id=?" + USERS.select.where(_.ID is User.loic.id).option[User](limit = true).sql shouldBe "SELECT u.id, u.name, u.email FROM users u WHERE u.id=? LIMIT 1" + } + } + describe("one") { + it("should build a list query") { + USERS.select.where(_.ID is User.loic.id).one[User].sql shouldBe "SELECT u.id, u.name, u.email FROM users u WHERE u.id=?" + } + } + describe("exists") { + it("should build a list query") { + USERS.select.where(_.ID is User.loic.id).exists[User].sql shouldBe "SELECT u.id, u.name, u.email FROM users u WHERE u.id=?" + } + } + it("should manipulate fields") { + USERS.select.dropFields(_.name != "id").all[User.Id].fields shouldBe List(USERS.ID) + USERS.select.dropFields(USERS.NAME, USERS.EMAIL).all[User.Id].fields shouldBe List(USERS.ID) + USERS.select.dropFields(USERS.NAME, USERS.EMAIL).prependFields(USERS.NAME).all[(String, User.Id)].fields shouldBe List(USERS.NAME, USERS.ID) + USERS.select.dropFields(USERS.NAME, USERS.EMAIL).appendFields(USERS.NAME).all[(User.Id, String)].fields shouldBe List(USERS.ID, USERS.NAME) + USERS.select.withoutFields(_.NAME).all[(User.Id, Option[String])].fields shouldBe List(USERS.ID, USERS.EMAIL) + } + it("should add limit and offset") { + USERS.select.offset(1).limit(2).all[User].sql shouldBe "SELECT u.id, u.name, u.email FROM users u LIMIT 2 OFFSET 1" + } } describe("Inner") { it("should compute the WHERE clause") { @@ -140,10 +186,13 @@ class QuerySpec extends BaseSpec { GroupByClause(List(POSTS.ID, POSTS.TITLE)).sql shouldBe " GROUP BY p.id, p.title" } it("should compute the HAVING clause") { + val cond = POSTS.CATEGORY.is(Category.Id(1)) + val filter = (Map("count" -> "true"), List(countFilter), ctx) HavingClause(None, None).sql shouldBe "" - HavingClause(Some(POSTS.CATEGORY.is(Category.Id(1))), None).sql shouldBe " HAVING p.category=?" - HavingClause(Some(POSTS.CATEGORY.is(Category.Id(1)) and POSTS.AUTHOR.is(User.Id(1))), None).sql shouldBe " HAVING p.category=? AND p.author=?" - HavingClause(None, Some((Map("count" -> "true"), List(countFilter), ctx))).sql shouldBe " HAVING COUNT(id) > ?" + HavingClause(Some(cond), None).sql shouldBe " HAVING p.category=?" + HavingClause(Some(cond and POSTS.AUTHOR.is(User.Id(1))), None).sql shouldBe " HAVING p.category=? AND p.author=?" + HavingClause(None, Some(filter)).sql shouldBe " HAVING COUNT(id) > ?" + HavingClause(Some(cond), Some(filter)).sql shouldBe " HAVING (p.category=?) AND (COUNT(id) > ?)" } it("should compute the ORDER BY clause") { OrderByClause(List(), None, nullsFirst = false).sql shouldBe "" diff --git a/src/test/scala/fr/loicknuchel/safeql/TableSpec.scala b/src/test/scala/fr/loicknuchel/safeql/TableSpec.scala index 69c4372..9af581e 100644 --- a/src/test/scala/fr/loicknuchel/safeql/TableSpec.scala +++ b/src/test/scala/fr/loicknuchel/safeql/TableSpec.scala @@ -1,11 +1,47 @@ package fr.loicknuchel.safeql -import fr.loicknuchel.safeql.models.NotImplementedJoin +import java.time.Instant + +import doobie.util.meta.Meta +import fr.loicknuchel.safeql.models.{ConflictingTableFields, NotImplementedJoin, UnknownTableFields} import fr.loicknuchel.safeql.testingutils.BaseSpec +import fr.loicknuchel.safeql.testingutils.Entities.{Post, User} import fr.loicknuchel.safeql.testingutils.database.Tables.{CATEGORIES, FEATURED, POSTS, USERS} class TableSpec extends BaseSpec { + protected implicit val instantMeta: Meta[Instant] = doobie.implicits.legacy.instant.JavaTimeInstantMeta + describe("Table") { + it("should find a field") { + POSTS.field[String]("id") shouldBe POSTS.ID + POSTS.id[String] shouldBe POSTS.ID // use select dynamic \o/ + an[UnknownTableFields[_]] should be thrownBy POSTS.field("miss") + + val joined = POSTS.joinOn(_.AUTHOR) + joined.field("title") shouldBe POSTS.TITLE + joined.field("name") shouldBe USERS.NAME + joined.name shouldBe USERS.NAME + an[UnknownTableFields[_]] should be thrownBy joined.field("miss") + an[ConflictingTableFields[_]] should be thrownBy POSTS.joinOn(_.AUTHOR).field("id") + + val unioned = POSTS.select.fields(POSTS.ID, POSTS.TITLE.as("name")).union(USERS.select.fields(USERS.ID, USERS.NAME)) + unioned.field("id") shouldBe TableField("id") + unioned.id shouldBe TableField("id") + an[UnknownTableFields[_]] should be thrownBy unioned.field("miss") + } + it("should check for a field presence") { + POSTS.has(POSTS.ID) shouldBe true + POSTS.has(USERS.ID) shouldBe false + // FIXME POSTS.dropFields(POSTS.ID).has(POSTS.ID) shouldBe false // because `has` check just schema & table name + } + it("should manipulate fields") { + USERS.dropFields(_.name != "id").select.all[User.Id].fields shouldBe List(USERS.ID) + USERS.dropFields(USERS.NAME, USERS.EMAIL).select.all[User.Id].fields shouldBe List(USERS.ID) + + val joined = POSTS.joinOn(_.AUTHOR) + joined.dropFields(_.name != "id").select.all[(Post.Id, User.Id)].fields shouldBe List(POSTS.ID, USERS.ID) + joined.dropFields(USERS.ID, USERS.NAME, USERS.EMAIL).select.all[Post].fields shouldBe POSTS.getFields + } describe("join") { it("should join two sql tables") { POSTS.join(USERS).on(POSTS.AUTHOR.is(USERS.ID)).sql shouldBe "posts p INNER JOIN users u ON p.author=u.id" diff --git a/src/test/scala/fr/loicknuchel/safeql/gen/GeneratorSpec.scala b/src/test/scala/fr/loicknuchel/safeql/gen/GeneratorSpec.scala index 6a7f360..2dc0ccd 100644 --- a/src/test/scala/fr/loicknuchel/safeql/gen/GeneratorSpec.scala +++ b/src/test/scala/fr/loicknuchel/safeql/gen/GeneratorSpec.scala @@ -3,16 +3,14 @@ package fr.loicknuchel.safeql.gen import java.util.UUID import fr.loicknuchel.safeql.gen.reader.H2Reader -import fr.loicknuchel.safeql.testingutils.{BaseSpec, CLI} +import fr.loicknuchel.safeql.gen.writer.ScalaWriter +import fr.loicknuchel.safeql.testingutils.{CLI, FileSpec} import fr.loicknuchel.safeql.utils.FileUtils import org.flywaydb.core.Flyway import org.flywaydb.core.internal.jdbc.DriverDataSource -import org.scalatest.BeforeAndAfterEach -class GeneratorSpec extends BaseSpec with BeforeAndAfterEach { - private val root = "target/tests-generator" - - override protected def afterEach(): Unit = FileUtils.delete(root).get +class GeneratorSpec extends FileSpec { + protected val root = "target/tests-GeneratorSpec" describe("Generator") { it("should generate the same files with all the generators") { @@ -37,7 +35,7 @@ class GeneratorSpec extends BaseSpec with BeforeAndAfterEach { // SQL files generator val sqlFilesPath = s"$root/sql-gen" - Generator.fromFiles(List("src/test/resources/sql_migrations/V1__test_schema.sql")).writer(CLI.GenerateSampleDatabase.writer.directory(sqlFilesPath)).generate().unsafeRunSync() + Generator.sqlFiles(List("src/test/resources/sql_migrations/V1__test_schema.sql")).writer(CLI.GenerateSampleDatabase.writer.directory(sqlFilesPath)).generate().unsafeRunSync() val sqlFilesDb = FileUtils.getDirContent(sqlFilesPath).get sqlFilesDb shouldBe basicDb } @@ -47,7 +45,16 @@ class GeneratorSpec extends BaseSpec with BeforeAndAfterEach { val flywayDb = FileUtils.getDirContent(flywayWriter.rootFolderPath).get val currentDb = FileUtils.getDirContent(CLI.GenerateSampleDatabase.writer.rootFolderPath).get - currentDb shouldBe flywayDb + currentDb.size shouldBe flywayDb.size + flywayDb.foreach { case (path, content) => currentDb.getOrElse(path, "") shouldBe content } + } + it("should set the writer before the reader") { + val writer = ScalaWriter() + val reader = H2Reader("url") + + Generator.writer(writer).reader(reader) shouldBe Generator.reader(reader).writer(writer) + // FIXME Generator.writer(writer).flyway("classpath:sql_migrations") shouldBe Generator.flyway("classpath:sql_migrations").writer(writer) + // FIXME Generator.writer(writer).sqlFiles(List("migrations.sql")) shouldBe Generator.sqlFiles(List("migrations.sql")).writer(writer) } } } diff --git a/src/test/scala/fr/loicknuchel/safeql/gen/reader/H2ReaderSpec.scala b/src/test/scala/fr/loicknuchel/safeql/gen/reader/H2ReaderSpec.scala index a14f00c..24a778f 100644 --- a/src/test/scala/fr/loicknuchel/safeql/gen/reader/H2ReaderSpec.scala +++ b/src/test/scala/fr/loicknuchel/safeql/gen/reader/H2ReaderSpec.scala @@ -18,6 +18,24 @@ class H2ReaderSpec extends BaseSpec with BeforeAndAfterAll { } describe("H2Reader") { + it("should have setters") { + val r = new H2Reader("url", "user", "pass", None, None) + + r.url shouldBe "url" + r.url("new").url shouldBe "new" + + r.user shouldBe "user" + r.user("new").user shouldBe "new" + + r.pass shouldBe "pass" + r.pass("new").pass shouldBe "new" + + r.schema shouldBe None + r.schema("new").schema shouldBe Some("new") + + r.excludes shouldBe None + r.excludes("new").excludes shouldBe Some("new") + } it("should read the database schema") { reader.read().unsafeRunSync() shouldBe Database(schemas = List(Database.Schema("PUBLIC", tables = List( Database.Table("PUBLIC", "posts", fields = List( diff --git a/src/test/scala/fr/loicknuchel/safeql/gen/writer/ScalaWriterSpec.scala b/src/test/scala/fr/loicknuchel/safeql/gen/writer/ScalaWriterSpec.scala index 06853e4..4bdfcf8 100644 --- a/src/test/scala/fr/loicknuchel/safeql/gen/writer/ScalaWriterSpec.scala +++ b/src/test/scala/fr/loicknuchel/safeql/gen/writer/ScalaWriterSpec.scala @@ -1,7 +1,8 @@ package fr.loicknuchel.safeql.gen.writer +import cats.data.NonEmptyList import fr.loicknuchel.safeql.gen.Database -import fr.loicknuchel.safeql.gen.Database.FieldRef +import fr.loicknuchel.safeql.gen.Database.{Field, FieldRef, Table} import fr.loicknuchel.safeql.gen.writer.ScalaWriter.TableConfig.Sort import fr.loicknuchel.safeql.gen.writer.ScalaWriter.{DatabaseConfig, FieldConfig, SchemaConfig, TableConfig} import fr.loicknuchel.safeql.testingutils.BaseSpec @@ -18,6 +19,21 @@ class ScalaWriterSpec extends BaseSpec { private val writer = ScalaWriter() describe("ScalaWriter") { + it("should have setters") { + val w = new ScalaWriter("dir", "pkg", Writer.IdentifierStrategy.KeepNames, DatabaseConfig()) + + w.directory shouldBe "dir" + w.directory("new").directory shouldBe "new" + + w.packageName shouldBe "pkg" + w.packageName("new").packageName shouldBe "new" + + w.identifierStrategy shouldBe Writer.IdentifierStrategy.KeepNames + w.identifierStrategy(Writer.IdentifierStrategy.UpperCase).identifierStrategy shouldBe Writer.IdentifierStrategy.UpperCase + + w.config shouldBe DatabaseConfig() + w.config(DatabaseConfig(imports = List("a"))).config shouldBe DatabaseConfig(imports = List("a")) + } it("should build needed paths") { writer.rootFolderPath shouldBe "src/main/scala/safeql" writer.listTablesFilePath shouldBe "src/main/scala/safeql/Tables.scala" @@ -47,12 +63,12 @@ class ScalaWriterSpec extends BaseSpec { describe("field attribute") { it("should generate a field attribute") { writer.tableFieldAttr(users, users.fields.head, DatabaseConfig()) shouldBe - "val ID: SqlField[Int, USERS] = SqlField(this, \"id\", \"INT NOT NULL\", JdbcType.Integer, nullable = false, 1)" + "val ID: SqlFieldRaw[Int, USERS] = SqlField(this, \"id\", \"INT NOT NULL\", JdbcType.Integer, nullable = false, 1)" } it("should put the custom type when defined") { val conf = DatabaseConfig(schemas = Map("PUBLIC" -> SchemaConfig(tables = Map("users" -> TableConfig(fields = Map("id" -> FieldConfig("User.Id"))))))) writer.tableFieldAttr(users, users.fields.head, conf) shouldBe - "val ID: SqlField[User.Id, USERS] = SqlField(this, \"id\", \"INT NOT NULL\", JdbcType.Integer, nullable = false, 1)" + "val ID: SqlFieldRaw[User.Id, USERS] = SqlField(this, \"id\", \"INT NOT NULL\", JdbcType.Integer, nullable = false, 1)" } it("should generate a reference field attribute") { writer.tableFieldAttr(posts, posts.fields(2), DatabaseConfig()) shouldBe @@ -133,6 +149,73 @@ class ScalaWriterSpec extends BaseSpec { "users" -> TableConfig(search = List("NotFound")) )))).getDatabaseErrors(db) shouldBe List("Field 'NotFound' in search of table 'PUBLIC.users' does not exist in Database") } + it("should access schema config by name or return empty one") { + val schema = SchemaConfig(tables = Map("users" -> TableConfig())) + val db = DatabaseConfig(schemas = Map("PUBLIC" -> schema)) + db.schema("PUBLIC") shouldBe schema + db.schema("NotFound") shouldBe SchemaConfig() + } + it("should access table config by name or return empty one") { + val table = TableConfig(fields = Map("id" -> FieldConfig())) + val schema = SchemaConfig(tables = Map("users" -> table)) + val db = DatabaseConfig(schemas = Map("PUBLIC" -> schema)) + db.table("PUBLIC", "users") shouldBe table + db.table("PUBLIC", "NotFound") shouldBe TableConfig() + db.table("NotFound", "users") shouldBe TableConfig() + db.table(Table("PUBLIC", "users", List())) shouldBe table + } + it("should access field config by name or return empty one") { + val field = FieldConfig("User.Id") + val table = TableConfig(fields = Map("id" -> field)) + val schema = SchemaConfig(tables = Map("users" -> table)) + val db = DatabaseConfig(schemas = Map("PUBLIC" -> schema)) + db.field("PUBLIC", "users", "id") shouldBe field + db.field("PUBLIC", "users", "NotFound") shouldBe FieldConfig() + db.field("PUBLIC", "NotFound", "id") shouldBe FieldConfig() + db.field("NotFound", "users", "id") shouldBe FieldConfig() + db.field(Table("PUBLIC", "users", List()), "id") shouldBe field + db.field(Field("PUBLIC", "users", "id", 1, "", "", nullable = true, 1, None, None)) shouldBe field + db.field(FieldRef("PUBLIC", "users", "id")) shouldBe field + } + } + describe("TableConfig") { + it("should have many constructors") { + val sort = TableConfig.Sort("-id") + val field = FieldConfig("User.Id") + TableConfig() shouldBe new TableConfig(None, List(), List(), Map()) + TableConfig("alias") shouldBe new TableConfig(Some("alias"), List(), List(), Map()) + TableConfig("alias", sort) shouldBe new TableConfig(Some("alias"), List(sort), List(), Map()) + TableConfig("alias", sort, List("id")) shouldBe new TableConfig(Some("alias"), List(sort), List("id"), Map()) + TableConfig("alias", sort, List("id"), Map("id" -> field)) shouldBe new TableConfig(Some("alias"), List(sort), List("id"), Map("id" -> field)) + TableConfig("alias", sort, Map("id" -> field)) shouldBe new TableConfig(Some("alias"), List(sort), List(), Map("id" -> field)) + TableConfig("alias", "-id", Map("id" -> field)) shouldBe new TableConfig(Some("alias"), List(sort), List(), Map("id" -> field)) + TableConfig("alias", "-id", List("id"), Map("id" -> field)) shouldBe new TableConfig(Some("alias"), List(sort), List("id"), Map("id" -> field)) + TableConfig("alias", Map("id" -> field)) shouldBe new TableConfig(Some("alias"), List(), List(), Map("id" -> field)) + } + describe("Sort") { + it("should have many constructors") { + TableConfig.Sort("id") shouldBe TableConfig.Sort("id", "id", NonEmptyList.of(TableConfig.Sort.Field("id", asc = true, None))) + TableConfig.Sort("User id", "id") shouldBe TableConfig.Sort("user-id", "User id", NonEmptyList.of(TableConfig.Sort.Field("id", asc = true, None))) + TableConfig.Sort("user", "User id", "id") shouldBe TableConfig.Sort("user", "User id", NonEmptyList.of(TableConfig.Sort.Field("id", asc = true, None))) + TableConfig.Sort("User id", NonEmptyList.of("id")) shouldBe TableConfig.Sort("user-id", "User id", NonEmptyList.of(TableConfig.Sort.Field("id", asc = true, None))) + } + describe("Field") { + it("should have many constructors") { + TableConfig.Sort.Field("id") shouldBe TableConfig.Sort.Field("id", asc = true, None) + TableConfig.Sort.Field("-id") shouldBe TableConfig.Sort.Field("id", asc = false, None) + TableConfig.Sort.Field("id", "? = 'Archived'") shouldBe TableConfig.Sort.Field("id", asc = true, Some("? = 'Archived'")) + TableConfig.Sort.Field("-id", "? = 'Archived'") shouldBe TableConfig.Sort.Field("id", asc = false, Some("? = 'Archived'")) + } + } + } + } + describe("FieldConfig") { + it("should have many constructors") { + FieldConfig() shouldBe FieldConfig(None, None) + FieldConfig(1) shouldBe FieldConfig(Some(1), None) + FieldConfig("User.Id") shouldBe FieldConfig(None, Some("User.Id")) + FieldConfig(1, "User.Id") shouldBe FieldConfig(Some(1), Some("User.Id")) + } } } } diff --git a/src/test/scala/fr/loicknuchel/safeql/gen/writer/WriterSpec.scala b/src/test/scala/fr/loicknuchel/safeql/gen/writer/WriterSpec.scala new file mode 100644 index 0000000..0c8987f --- /dev/null +++ b/src/test/scala/fr/loicknuchel/safeql/gen/writer/WriterSpec.scala @@ -0,0 +1,43 @@ +package fr.loicknuchel.safeql.gen.writer + +import fr.loicknuchel.safeql.gen.Database +import fr.loicknuchel.safeql.gen.Database.FieldRef +import fr.loicknuchel.safeql.testingutils.FileSpec + +class WriterSpec extends FileSpec { + protected val root = "target/tests-WriterSpec" + private val writer = ScalaWriter(directory = root) + private val users = Database.Table("PUBLIC", "users", fields = List( + Database.Field("PUBLIC", "users", "id", 4, "INTEGER", "INT NOT NULL", nullable = false, 1, None, None), + Database.Field("PUBLIC", "users", "name", 12, "VARCHAR", "VARCHAR(50)", nullable = true, 2, None, None))) + private val posts = Database.Table("PUBLIC", "posts", fields = List( + Database.Field("PUBLIC", "posts", "id", 4, "INTEGER", "INT NOT NULL", nullable = false, 1, None, None), + Database.Field("PUBLIC", "posts", "title", 12, "VARCHAR", "VARCHAR(50)", nullable = true, 2, None, None), + Database.Field("PUBLIC", "posts", "author", 4, "INTEGER", "INT NOT NULL", nullable = false, 3, None, Some(FieldRef("PUBLIC", "users", "id"))))) + private val db = Database(schemas = List(Database.Schema("PUBLIC", tables = List(posts, users)))) + + describe("Writer") { + it("should be able to read written files") { + writer.write(db).get + val written = writer.readFiles().get + val generated = writer.generateFiles(db) + written shouldBe generated + } + describe("IdentifierStrategy") { + describe("KeepNames") { + it("should escape scala keywords") { + val s = Writer.IdentifierStrategy.KeepNames + s.format("val") shouldBe "`val`" + s.format("var") shouldBe "`var`" + s.format("def") shouldBe "`def`" + s.format("type") shouldBe "`type`" + s.format("class") shouldBe "`class`" + s.format("object") shouldBe "`object`" + s.format("import") shouldBe "`import`" + s.format("package") shouldBe "`package`" + s.format("other") shouldBe "other" + } + } + } + } +} diff --git a/src/test/scala/fr/loicknuchel/safeql/models/PageSpec.scala b/src/test/scala/fr/loicknuchel/safeql/models/PageSpec.scala index d7c9385..406ab0b 100644 --- a/src/test/scala/fr/loicknuchel/safeql/models/PageSpec.scala +++ b/src/test/scala/fr/loicknuchel/safeql/models/PageSpec.scala @@ -28,6 +28,7 @@ class PageSpec extends BaseSpec { p.nullsFirst shouldBe false p.withNullsFirst.nullsFirst shouldBe true + p.withNullsLast.nullsFirst shouldBe false } it("should clean arguments on orderBy setter") { val p = Page.Params() diff --git a/src/test/scala/fr/loicknuchel/safeql/samples/GeneratorSamples.scala b/src/test/scala/fr/loicknuchel/safeql/samples/GeneratorSamples.scala index 0d30382..63d4895 100644 --- a/src/test/scala/fr/loicknuchel/safeql/samples/GeneratorSamples.scala +++ b/src/test/scala/fr/loicknuchel/safeql/samples/GeneratorSamples.scala @@ -18,7 +18,7 @@ object GeneratorSamples { def generateFromSQLFiles(): Unit = { Generator - .fromFiles(List("src/test/resources/sql_migrations/V1__test_schema.sql")) + .sqlFiles(List("src/test/resources/sql_migrations/V1__test_schema.sql")) .writer(ScalaWriter(packageName = "com.company.db")) .generate().unsafeRunSync() } diff --git a/src/test/scala/fr/loicknuchel/safeql/samples/QuerySamples.scala b/src/test/scala/fr/loicknuchel/safeql/samples/QuerySamples.scala index 4722b31..f618a97 100644 --- a/src/test/scala/fr/loicknuchel/safeql/samples/QuerySamples.scala +++ b/src/test/scala/fr/loicknuchel/safeql/samples/QuerySamples.scala @@ -1,11 +1,15 @@ package fr.loicknuchel.safeql.samples -import doobie.implicits.legacy.instant.JavaTimeInstantMeta // needed to decode Instant +import java.time.Instant + +import doobie.util.meta.Meta import fr.loicknuchel.safeql.testingutils.Entities.{Post, User} import fr.loicknuchel.safeql.testingutils.SqlSpec import fr.loicknuchel.safeql.testingutils.database.Tables.{POSTS, USERS} class QuerySamples extends SqlSpec { + protected implicit val instantMeta: Meta[Instant] = doobie.implicits.legacy.instant.JavaTimeInstantMeta + it("should perform a basic select") { val users: List[User] = USERS.select.all[User].run(xa).unsafeRunSync() users shouldBe User.all diff --git a/src/test/scala/fr/loicknuchel/safeql/testingutils/CLI.scala b/src/test/scala/fr/loicknuchel/safeql/testingutils/CLI.scala index cb3814d..3e24d3a 100644 --- a/src/test/scala/fr/loicknuchel/safeql/testingutils/CLI.scala +++ b/src/test/scala/fr/loicknuchel/safeql/testingutils/CLI.scala @@ -16,7 +16,7 @@ object CLI { val writer: ScalaWriter = ScalaWriter( directory = "src/test/scala", packageName = "fr.loicknuchel.safeql.testingutils.database", - identifierStrategy = Writer.IdentifierStrategy.upperCase, + identifierStrategy = Writer.IdentifierStrategy.UpperCase, config = DatabaseConfig( scaladoc = _ => Some("Hello"), imports = List("fr.loicknuchel.safeql.testingutils.Entities._"), diff --git a/src/test/scala/fr/loicknuchel/safeql/testingutils/Entities.scala b/src/test/scala/fr/loicknuchel/safeql/testingutils/Entities.scala index 0fdf2ba..c45d7fc 100644 --- a/src/test/scala/fr/loicknuchel/safeql/testingutils/Entities.scala +++ b/src/test/scala/fr/loicknuchel/safeql/testingutils/Entities.scala @@ -50,10 +50,10 @@ object Entities { val all = List(newYear, first2020, sqlQueries) } - case class Kind(char: String, varchar: String, timestamp: Instant, date: LocalDate, boolean: Boolean, int: Int, bigint: Long, double: Double, a_long_name: Int) + case class Kind(char: String, varchar: String, timestamp: Instant, date: LocalDate, boolean: Boolean, int: Int, smallint: Short, bigint: Long, double: Double, a_long_name: Int) object Kind { - val one: Kind = Kind("char", "varchar", Instant.ofEpochSecond(1596615600), LocalDate.of(2020, 8, 5), boolean = true, 1, 10, 4.5, 0) + val one: Kind = Kind("char", "varchar", Instant.ofEpochSecond(1596615600), LocalDate.of(2020, 8, 5), boolean = true, 1, 4, 10, 4.5, 0) val all = List(one) } diff --git a/src/test/scala/fr/loicknuchel/safeql/testingutils/FileSpec.scala b/src/test/scala/fr/loicknuchel/safeql/testingutils/FileSpec.scala new file mode 100644 index 0000000..e5c9cf9 --- /dev/null +++ b/src/test/scala/fr/loicknuchel/safeql/testingutils/FileSpec.scala @@ -0,0 +1,12 @@ +package fr.loicknuchel.safeql.testingutils + +import fr.loicknuchel.safeql.utils.FileUtils +import org.scalatest.BeforeAndAfterEach + +abstract class FileSpec extends BaseSpec with BeforeAndAfterEach { + protected val root: String + + override protected def beforeEach(): Unit = FileUtils.mkdirs(root).get + + override protected def afterEach(): Unit = FileUtils.delete(root).get +} diff --git a/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/CATEGORIES.scala b/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/CATEGORIES.scala index b6b6865..f826ac8 100644 --- a/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/CATEGORIES.scala +++ b/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/CATEGORIES.scala @@ -13,8 +13,8 @@ import fr.loicknuchel.safeql.testingutils.Entities._ class CATEGORIES private(getAlias: Option[String] = Some("c")) extends Table.SqlTable("PUBLIC", "categories", getAlias) { type Self = CATEGORIES - val ID: SqlField[Category.Id, CATEGORIES] = SqlField(this, "id", "INT NOT NULL", JdbcType.Integer, nullable = false, 1) - val NAME: SqlField[String, CATEGORIES] = SqlField(this, "name", "VARCHAR(50) NOT NULL", JdbcType.VarChar, nullable = false, 2) + val ID: SqlFieldRaw[Category.Id, CATEGORIES] = SqlField(this, "id", "INT NOT NULL", JdbcType.Integer, nullable = false, 1) + val NAME: SqlFieldRaw[String, CATEGORIES] = SqlField(this, "name", "VARCHAR(50) NOT NULL", JdbcType.VarChar, nullable = false, 2) override def getFields: List[SqlField[_, CATEGORIES]] = List(ID, NAME) diff --git a/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/FEATURED.scala b/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/FEATURED.scala index 6732d7a..27a7e44 100644 --- a/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/FEATURED.scala +++ b/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/FEATURED.scala @@ -16,8 +16,8 @@ class FEATURED private(getAlias: Option[String] = None) extends Table.SqlTable(" val POST_ID: SqlFieldRef[Post.Id, FEATURED, POSTS] = SqlField(this, "post_id", "INT NOT NULL", JdbcType.Integer, nullable = false, 1, POSTS.table.ID) val BY: SqlFieldRef[User.Id, FEATURED, USERS] = SqlField(this, "by", "INT NOT NULL", JdbcType.Integer, nullable = false, 2, USERS.table.ID) - val START: SqlField[Instant, FEATURED] = SqlField(this, "start", "TIMESTAMP NOT NULL", JdbcType.Timestamp, nullable = false, 3) - val STOP: SqlField[Instant, FEATURED] = SqlField(this, "stop", "TIMESTAMP NOT NULL", JdbcType.Timestamp, nullable = false, 4) + val START: SqlFieldRaw[Instant, FEATURED] = SqlField(this, "start", "TIMESTAMP NOT NULL", JdbcType.Timestamp, nullable = false, 3) + val STOP: SqlFieldRaw[Instant, FEATURED] = SqlField(this, "stop", "TIMESTAMP NOT NULL", JdbcType.Timestamp, nullable = false, 4) override def getFields: List[SqlField[_, FEATURED]] = List(POST_ID, BY, START, STOP) diff --git a/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/KINDS.scala b/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/KINDS.scala index 6daf347..055bf56 100644 --- a/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/KINDS.scala +++ b/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/KINDS.scala @@ -14,21 +14,22 @@ import fr.loicknuchel.safeql.testingutils.Entities._ class KINDS private(getAlias: Option[String] = None) extends Table.SqlTable("PUBLIC", "kinds", getAlias) { type Self = KINDS - val CHAR: SqlField[String, KINDS] = SqlField(this, "char", "CHAR(4)", JdbcType.Char, nullable = true, 1) - val VARCHAR: SqlField[String, KINDS] = SqlField(this, "varchar", "VARCHAR(50)", JdbcType.VarChar, nullable = true, 2) - val TIMESTAMP: SqlField[Instant, KINDS] = SqlField(this, "timestamp", "TIMESTAMP", JdbcType.Timestamp, nullable = true, 3) - val DATE: SqlField[LocalDate, KINDS] = SqlField(this, "date", "DATE", JdbcType.Date, nullable = true, 4) - val BOOLEAN: SqlField[Boolean, KINDS] = SqlField(this, "boolean", "BOOLEAN", JdbcType.Boolean, nullable = true, 5) - val INT: SqlField[Int, KINDS] = SqlField(this, "int", "INT", JdbcType.Integer, nullable = true, 6) - val BIGINT: SqlField[Long, KINDS] = SqlField(this, "bigint", "BIGINT", JdbcType.BigInt, nullable = true, 7) - val DOUBLE: SqlField[Double, KINDS] = SqlField(this, "double", "DOUBLE PRECISION", JdbcType.Double, nullable = true, 8) - val A_LONG_NAME: SqlField[Int, KINDS] = SqlField(this, "a_long_name", "INT", JdbcType.Integer, nullable = true, 9) - - override def getFields: List[SqlField[_, KINDS]] = List(CHAR, VARCHAR, TIMESTAMP, DATE, BOOLEAN, INT, BIGINT, DOUBLE, A_LONG_NAME) + val CHAR: SqlFieldRaw[String, KINDS] = SqlField(this, "char", "CHAR(4)", JdbcType.Char, nullable = true, 1) + val VARCHAR: SqlFieldRaw[String, KINDS] = SqlField(this, "varchar", "VARCHAR(50)", JdbcType.VarChar, nullable = true, 2) + val TIMESTAMP: SqlFieldRaw[Instant, KINDS] = SqlField(this, "timestamp", "TIMESTAMP", JdbcType.Timestamp, nullable = true, 3) + val DATE: SqlFieldRaw[LocalDate, KINDS] = SqlField(this, "date", "DATE", JdbcType.Date, nullable = true, 4) + val BOOLEAN: SqlFieldRaw[Boolean, KINDS] = SqlField(this, "boolean", "BOOLEAN", JdbcType.Boolean, nullable = true, 5) + val INT: SqlFieldRaw[Int, KINDS] = SqlField(this, "int", "INT", JdbcType.Integer, nullable = true, 6) + val SMALLINT: SqlFieldRaw[Short, KINDS] = SqlField(this, "smallint", "SMALLINT", JdbcType.SmallInt, nullable = true, 7) + val BIGINT: SqlFieldRaw[Long, KINDS] = SqlField(this, "bigint", "BIGINT", JdbcType.BigInt, nullable = true, 8) + val DOUBLE: SqlFieldRaw[Double, KINDS] = SqlField(this, "double", "DOUBLE PRECISION", JdbcType.Double, nullable = true, 9) + val A_LONG_NAME: SqlFieldRaw[Int, KINDS] = SqlField(this, "a_long_name", "INT", JdbcType.Integer, nullable = true, 10) + + override def getFields: List[SqlField[_, KINDS]] = List(CHAR, VARCHAR, TIMESTAMP, DATE, BOOLEAN, INT, SMALLINT, BIGINT, DOUBLE, A_LONG_NAME) override def getSorts: List[Sort] = List() - override def searchOn: List[SqlField[_, KINDS]] = List(CHAR, VARCHAR, TIMESTAMP, DATE, BOOLEAN, INT, BIGINT, DOUBLE, A_LONG_NAME) + override def searchOn: List[SqlField[_, KINDS]] = List(CHAR, VARCHAR, TIMESTAMP, DATE, BOOLEAN, INT, SMALLINT, BIGINT, DOUBLE, A_LONG_NAME) override def getFilters: List[Filter] = List() diff --git a/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/POSTS.scala b/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/POSTS.scala index a1f640f..5d0b20c 100644 --- a/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/POSTS.scala +++ b/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/POSTS.scala @@ -14,10 +14,10 @@ import fr.loicknuchel.safeql.testingutils.Entities._ class POSTS private(getAlias: Option[String] = Some("p")) extends Table.SqlTable("PUBLIC", "posts", getAlias) { type Self = POSTS - val ID: SqlField[Post.Id, POSTS] = SqlField(this, "id", "INT NOT NULL", JdbcType.Integer, nullable = false, 1) - val TITLE: SqlField[String, POSTS] = SqlField(this, "title", "VARCHAR(50) NOT NULL", JdbcType.VarChar, nullable = false, 2) - val TEXT: SqlField[String, POSTS] = SqlField(this, "text", "VARCHAR(4096) NOT NULL", JdbcType.VarChar, nullable = false, 3) - val DATE: SqlField[Instant, POSTS] = SqlField(this, "date", "TIMESTAMP NOT NULL", JdbcType.Timestamp, nullable = false, 4) + val ID: SqlFieldRaw[Post.Id, POSTS] = SqlField(this, "id", "INT NOT NULL", JdbcType.Integer, nullable = false, 1) + val TITLE: SqlFieldRaw[String, POSTS] = SqlField(this, "title", "VARCHAR(50) NOT NULL", JdbcType.VarChar, nullable = false, 2) + val TEXT: SqlFieldRaw[String, POSTS] = SqlField(this, "text", "VARCHAR(4096) NOT NULL", JdbcType.VarChar, nullable = false, 3) + val DATE: SqlFieldRaw[Instant, POSTS] = SqlField(this, "date", "TIMESTAMP NOT NULL", JdbcType.Timestamp, nullable = false, 4) val AUTHOR: SqlFieldRef[User.Id, POSTS, USERS] = SqlField(this, "author", "INT NOT NULL", JdbcType.Integer, nullable = false, 5, USERS.table.ID) val CATEGORY: SqlFieldRef[Category.Id, POSTS, CATEGORIES] = SqlField(this, "category", "INT", JdbcType.Integer, nullable = true, 6, CATEGORIES.table.ID) diff --git a/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/USERS.scala b/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/USERS.scala index 52fab05..8c19048 100644 --- a/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/USERS.scala +++ b/src/test/scala/fr/loicknuchel/safeql/testingutils/database/tables/USERS.scala @@ -12,9 +12,9 @@ import fr.loicknuchel.safeql.testingutils.Entities._ class USERS private(getAlias: Option[String] = Some("u")) extends Table.SqlTable("PUBLIC", "users", getAlias) { type Self = USERS - val ID: SqlField[User.Id, USERS] = SqlField(this, "id", "INT NOT NULL", JdbcType.Integer, nullable = false, 1) - val NAME: SqlField[String, USERS] = SqlField(this, "name", "VARCHAR(50) NOT NULL", JdbcType.VarChar, nullable = false, 2) - val EMAIL: SqlField[String, USERS] = SqlField(this, "email", "VARCHAR(50)", JdbcType.VarChar, nullable = true, 3) + val ID: SqlFieldRaw[User.Id, USERS] = SqlField(this, "id", "INT NOT NULL", JdbcType.Integer, nullable = false, 1) + val NAME: SqlFieldRaw[String, USERS] = SqlField(this, "name", "VARCHAR(50) NOT NULL", JdbcType.VarChar, nullable = false, 2) + val EMAIL: SqlFieldRaw[String, USERS] = SqlField(this, "email", "VARCHAR(50)", JdbcType.VarChar, nullable = true, 3) override def getFields: List[SqlField[_, USERS]] = List(ID, NAME, EMAIL) diff --git a/src/test/scala/fr/loicknuchel/safeql/utils/FileUtilsSpec.scala b/src/test/scala/fr/loicknuchel/safeql/utils/FileUtilsSpec.scala index 4461191..12e35d9 100644 --- a/src/test/scala/fr/loicknuchel/safeql/utils/FileUtilsSpec.scala +++ b/src/test/scala/fr/loicknuchel/safeql/utils/FileUtilsSpec.scala @@ -1,14 +1,9 @@ package fr.loicknuchel.safeql.utils -import fr.loicknuchel.safeql.testingutils.BaseSpec -import org.scalatest.BeforeAndAfterEach +import fr.loicknuchel.safeql.testingutils.FileSpec -class FileUtilsSpec extends BaseSpec with BeforeAndAfterEach { - private val root = "target/tests-file-utils" - - override protected def beforeEach(): Unit = FileUtils.mkdirs(root).get - - override protected def afterEach(): Unit = FileUtils.delete(root).get +class FileUtilsSpec extends FileSpec { + protected val root = "target/tests-FileUtilsSpec" describe("FileUtils") { it("should compute parent path") { @@ -35,6 +30,27 @@ class FileUtilsSpec extends BaseSpec with BeforeAndAfterEach { FileUtils.delete(s"$root/src").get an[Exception] should be thrownBy FileUtils.read(s"$root/src/test/scala/fr/lkn/main.scala").get } + it("should read a file with correct line breaks") { + val noEmptyEndLine = + """Bonjour, + |ça va ?""".stripMargin + FileUtils.write(s"$root/test.txt", noEmptyEndLine).get + FileUtils.read(s"$root/test.txt").get shouldBe noEmptyEndLine + + val emptyEndLine = + """Bonjour, + |ça va ? + |""".stripMargin + FileUtils.write(s"$root/test.txt", emptyEndLine).get + FileUtils.read(s"$root/test.txt").get shouldBe emptyEndLine + + val manyEmptyEndLines = + """Bonjour, + | + |""".stripMargin + FileUtils.write(s"$root/test.txt", manyEmptyEndLines).get + FileUtils.read(s"$root/test.txt").get shouldBe manyEmptyEndLines + } it("should list folder content") { FileUtils.mkdirs(s"$root/src/main/scala/fr/loicknuchel").get FileUtils.write(s"$root/src/main/scala/fr/loicknuchel/Main.scala", "public class Main").get