├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt ├── renovate.json ├── src ├── main │ └── scala │ │ └── com │ │ └── github │ │ └── tarao │ │ └── slickjdbc │ │ ├── getresult │ │ └── GetResult.scala │ │ ├── interpolation │ │ ├── Interpolation.scala │ │ ├── MacroTreeBuilder.scala │ │ ├── Placeholder.scala │ │ ├── SetParameter.scala │ │ └── ToPlaceholder.scala │ │ └── query │ │ └── Translator.scala └── test │ ├── resources │ └── application.conf │ └── scala │ └── com │ └── github │ └── tarao │ └── slickjdbc │ ├── IntegrationSpec.scala │ ├── getresult │ ├── GetResultSpec.scala │ └── TypeBinderSpec.scala │ ├── helper │ ├── TestDB.scala │ ├── TraitSingletonBehavior.scala │ └── UnitSpec.scala │ ├── interpolation │ ├── InterpolationSpec.scala │ ├── LiteralSpec.scala │ └── PlaceholderSpec.scala │ └── query │ └── TranslatorSpec.scala └── version.sbt /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: [ "main" ] 6 | jobs: 7 | test: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | include: 12 | - java: 8 13 | - java: 11 14 | - java: 17 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | - name: Setup JDK 20 | uses: actions/setup-java@v3 21 | with: 22 | distribution: temurin 23 | java-version: ${{ matrix.java }} 24 | cache: 'sbt' 25 | - name: Build and Test 26 | run: sbt +test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | .bloop 16 | .bsp 17 | .metals 18 | metals.sbt 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 INA Lintaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | slick-jdbc-extension [![CI][ci-img]][ci] [![Maven Central][maven-img]][maven] 2 | ==================== 3 | 4 | An extension to `slick.jdbc`, [Slick][slick]'s plain SQL queries, 5 | including the following features. 6 | 7 | - [More types in SQL interpolation](#setparameter) 8 | - Literal parameters 9 | - Non-empty lists 10 | - Case classes (products) 11 | - [Extensible raw query translator](#translator) 12 | - Embedding caller information as a comment 13 | - Stripping margin 14 | - [Result mapper by column name](#getresult) 15 | 16 | These features can be selectively enabled. 17 | 18 | ## Getting started 19 | 20 | Add dependency in your `build.sbt` as the following. 21 | 22 | ```scala 23 | libraryDependencies ++= Seq( 24 | ... 25 | "com.github.tarao" %% "slick-jdbc-extension" % "0.1.0" 26 | ) 27 | ``` 28 | 29 | The library is available on [Maven Central][maven]. Currently, 30 | supported Scala versions are 2.12, and 2.13. 31 | 32 | ## Overview 33 | 34 | [Slick][slick] supports plain SQL queries but it lacks some essential 35 | features. For example, there is no way to bind a list parameter to a 36 | prepared statement. 37 | 38 | This extension provides additional features to Slick-style plain SQL 39 | queries. The extension consists of a family of traits and each of 40 | them can be enabled selectively. The easiest way to use the extension 41 | is to define a class with the traits. 42 | 43 | Let's see an example of the usage of the extension. To start with, we 44 | assume to have an abstract class to run a DB query something like 45 | this. 46 | 47 | ```scala 48 | abstract class DBHandler { 49 | import scala.concurrent.{Future, Await} 50 | import scala.concurrent.duration.Duration 51 | import slick.dbio.{DBIOAction, NoStream, Effect} 52 | import slick.backend.DatabaseComponent 53 | 54 | type Database = DatabaseComponent#DatabaseDef 55 | 56 | def run[R](a: DBIOAction[R, NoStream, Nothing])(implicit db: Database): R = 57 | Await.result(db.run(a), Duration.Inf) 58 | } 59 | ``` 60 | 61 | Then a repository class to use plain SQL queries with the extension will look like this. (We are not interested in how the concrete DB object is supplied.) 62 | 63 | ```scala 64 | import com.github.tarao.slickjdbc._ 65 | import interpolation.{SQLInterpolation, CompoundParameter, TableName} 66 | import getresult.{GetResult, AutoUnwrapOption} 67 | 68 | class SampleRepository extends DBHandler 69 | with SQLInterpolation with CompoundParameter 70 | with GetResult with AutoUnwrapOption { 71 | implicit def db: Database = ??? 72 | 73 | ... 74 | } 75 | ``` 76 | 77 | `SQLInterpolation`, `CompoundParameter`, `GetResult` and 78 | `AutoUnwrapOption` are feature selectors of the extension. 79 | 80 | For the rest of the example, we use `Entry` model class defined as the 81 | following. 82 | 83 | ```scala 84 | case class Entry(id: Long, url: String) 85 | ``` 86 | 87 | To define a mapping from a column name to a field of the model in the 88 | repository class, use `getResult` method defined in `GetResult` trait. 89 | 90 | ```scala 91 | implicit val getEntryResult = getResult { Entry( 92 | column("entry_id"), 93 | column("url") 94 | ) } 95 | ``` 96 | 97 | This will map `entry_id` column of a query result to the first field 98 | of `Entry` and `url` column to the second. This is very similar to 99 | Slick's `GetResult.apply`. The type of the column is inferred by the 100 | field type of `Entry.id` (it is `Long` in this case). Note that this 101 | example also uses `AutoUnwrapOption` to convert from `Option[Long]` to 102 | `Long`. The default column type is `Option[_]`ed since the resulting 103 | column value may be `NULL`. 104 | 105 | Let's see an example of SQL interpolation. In addition to Slick's SQL 106 | interpolation, we can specify a structured value directly as a 107 | parameter. 108 | 109 | ```scala 110 | val table = TableName("entry") 111 | 112 | def add(entry: Entry) = 113 | run { sqlu""" 114 | | INSERT INTO ${table} (entry_id, url) 115 | | VALUES ${entry} 116 | """ 117 | } 118 | ``` 119 | 120 | This actually illustrates three things. 121 | 122 | - You can specify a table name by a value of `TableName`. This value 123 | will be embedded literally in the raw query. 124 | - A value of case class (`entry` in this case) is expanded to 125 | placeholders like `(?, ?)` and the fields of the value are bound 126 | to them. 127 | - Characters before `|` is stripped in the raw query. 128 | 129 | You can also specify a list as a parameter. But in this case, you 130 | must say that it is not empty. 131 | 132 | ```scala 133 | import eu.timepit.refined.api.Refined 134 | import eu.timepit.refined.collection.NonEmpty 135 | 136 | def findAll(entryIds: Seq[Long] Refined NonEmpty): Seq[Entry] = run { 137 | sql""" 138 | | SELECT * FROM ${table} 139 | | WHERE entry_id IN $ids 140 | """.as[Entry] 141 | } 142 | ``` 143 | 144 | `$ids` must be refined by `NonEmpty` and it expands to `(?, ?, ..., 145 | ?)`. You can use `refineV` to refine values. 146 | 147 | ```scala 148 | import eu.timepit.refined.collection.NonEmpty 149 | import eu.timepit.refined.refineV 150 | 151 | val idsOrError = refineV[NonEmpty](Seq(101L, 102L, 103L)) 152 | val entries = ids match { 153 | case Right(ids) => repository.findAll(ids) 154 | case Left(_) => sys.error("Never happen in this case") 155 | } 156 | ``` 157 | 158 | See the documentation of [refined][] for the detail about refined 159 | types. 160 | 161 | If you were using [nonempty][] from older versions, it still works as 162 | expected because `nonempty.NonEmpty` is a kind of refined collection 163 | type. 164 | 165 | ```scala 166 | import com.github.tarao.nonempty.NonEmpty 167 | 168 | def findAll(entryIds: Option[NonEmpty[Long]]): Seq[Entry] = entryIds match { 169 | case Some(ids) => run { 170 | sql""" 171 | | SELECT * FROM ${table} 172 | | WHERE entry_id IN $ids 173 | """.as[Entry] 174 | } 175 | case None => Seq.empty 176 | } 177 | ``` 178 | 179 | In this case, you call `findAll` method like the following since there is an implicit conversion from `Seq[Long]` to `Option[NonEmpty[Long]]`. 180 | 181 | ```scala 182 | repository.findAll(Seq(101L, 102L, 103L)) 183 | ``` 184 | 185 | See the documentation of [nonempty][] for the detail. 186 | 187 | ## Additional types in SQL interpolation 188 | 189 | To use the extended SQL interpolation, you have to first enable it. 190 | To enable the interpolation inside a particular trait or class, add 191 | `SQLInterpolation` trait to the trait or class. 192 | 193 | ```scala 194 | import com.github.tarao.slickjdbc._ 195 | class YourRepositoryClass extends interpolation.SQLInterpolation 196 | ``` 197 | 198 | If you want to explicitly `import` the feature, then `import` methods 199 | in `SQLInterpolation` object. 200 | 201 | ```scala 202 | import com.github.tarao.slickjdbc._ 203 | import interpolation.SQLInterpolation._ 204 | ``` 205 | 206 | If you are `import`ing a Slick driver API, doing that by `import 207 | slick.driver.SomeDriver.api._` causes a conflict with the extended SQL 208 | interpolation since the API also defines a SQL interpolation of Slick. 209 | In this case, `import` only desired features to avoid the conflict. 210 | For example, if you are using `Database` from the API, import it as 211 | `import slick.driver.SomeDriver.api.Database`. 212 | 213 | ### Literal parameters 214 | 215 | When a value with `Literal` is passed as a parameter of the 216 | interpolation, a string returned by `toString` on the value is 217 | embedded literally in the raw query as if it was embedded by `#${}`. 218 | There are two predefined `Literal`s. 219 | 220 | - `SimpleString` 221 | - takes a string as a parameter of the constructor and embed that string. 222 | - `TableName` 223 | - takes a name as a parameter of the constructor and embed that name. 224 | 225 | Note that a `Literal` value is **not** expanded to a placeholder `?`. 226 | If the value needs to be escaped as a part of a SQL string, then you 227 | should do it by yourself. 228 | 229 | ### Non-empty lists 230 | 231 | This feature requires `interpolation.ComboundParameter` trait or 232 | `import interpolation.ComboundParameter._`. 233 | 234 | When a non-empty list is passed as a parameter of the interpolation, 235 | the value expands to `?`s in the prepared statement, and items in the 236 | value are bound to the statement. 237 | 238 | For example, if you have `val Right(ne) = refineV[NonEmpty](Seq(1, 2, 239 | 3))` and embed it as `sql"... ($ne) ..."`, the raw query will be 240 | `... (?, ?, ?) ...` and `1`, `2`, `3` are bound to the placeholders. 241 | The resulting raw query will be the same if you omit parentheses like 242 | `sql"... $ne ..."`. 243 | 244 | If you pass a non-empty list of tuples, for example pairs, it expands 245 | to `(?, ?), (?, ?), ..., (?, ?)`. This is useful to `INSERT` multiple 246 | values by `INSERT INTO table (col1, col2) VALUES $aListOfPairs`. 247 | 248 | ### Case classes (products) 249 | 250 | This feature requires `interpolation.ComboundParameter` trait or 251 | `import interpolation.ComboundParameter._`. 252 | 253 | When a product especially an instance of a case class is passed as a 254 | parameter of the interpolation, the value expands to `?`s in the 255 | prepared statement, and fields in the value are bound to the 256 | statement. 257 | 258 | For example, if you have `val p = SomeCaseClass(1, "foo")` and embed 259 | it as `sql"... ($p) ..."`, the raw query will be `... (?, ?) ...` and 260 | `1`, `"foo"` are bound to the placeholders. The resulting raw query 261 | will be the same if you omit parentheses like `sql"... $p ..."`. 262 | 263 | If you have another product value in a field of the passed value, it 264 | also be expanded and the placeholders are flattened. 265 | 266 | You can also put a product value in a non-empty list and it has the 267 | same effect as putting tuples **except when the arity of the product 268 | is 1**. When the product arity is exactly 1, the value is treated as 269 | if it is a primitive value. In other words, a value of `Seq[[Single] 270 | Refined NonEmpty` (where `Single` is a `Product` of arity 1) expands 271 | to `(?, ?, ..., ?)` not to `(?), (?), ..., (?)`. In contrast, a value 272 | of `Seq[Tuple1[_]] Refined NonEmpty` expands to `(?), (?), ..., (?)` 273 | not to `(?, ?, ..., ?)`. 274 | 275 | Note that passing `Option[_]` as an interpolation parameter is 276 | statically rejected and passing a product of arity zero (this can be 277 | made by for example `case class Empty()`) causes 278 | `java.sql.SQLException` at runtime. 279 | 280 | ## Raw query translator 281 | 282 | To use this feature, you have to enable the extended SQL 283 | interpolation. Follow the instruction [above](#setparameter) to 284 | enable it. 285 | 286 | The query translator translates a raw query generated by the 287 | interpolation. A translation is simply from string to string and you 288 | can stack multiple translators. 289 | 290 | ### The default translators 291 | 292 | - `MarginStripper` 293 | - Strip characters before `|` from each line of a query string. 294 | This makes a query log a bit more readable. 295 | - `CallerCommenter` 296 | - Record the code position that the interpolation is invoked as a 297 | comment of SQL query. You can trace a line number, a file name 298 | and a method name in a class from an ordinary query log on the 299 | DB server. 300 | 301 | ### Custom translators 302 | 303 | You can use a custom translator stack by defining an implicit value of 304 | `Iterable[query.Translator]`. For example, if you want to add a 305 | translator after the default translators, define an implicit value in 306 | a static scope where you invoke the SQL interpolation. 307 | 308 | ```scala 309 | import com.github.tarao.slickjdbc._ 310 | 311 | implicit val translators: Iterable[query.Translator] = Seq( 312 | query.MarginStripper, 313 | query.CallerCommenter, 314 | new query.Translator { 315 | def apply(query: String, context: query.Context) = 316 | ??? /* return a translated string */ 317 | } 318 | ) 319 | ``` 320 | 321 | ## Result mapper by column name 322 | 323 | This feature requires `getresult.GetResult` trait or object. 324 | 325 | With the trait, you can use `getResult` method and can use `column` 326 | method in the block of `getResult` without a receiver like in the 327 | example in [Overview](#overview) section. 328 | 329 | If you prefer the object, you just need to `import` it. 330 | 331 | ```scala 332 | import com.github.tarao.slickjdbc._ 333 | import getresult.GetResult 334 | ``` 335 | 336 | The usage of `GetResult` is quite similar to that of Slick's 337 | `GetResult`. You will receive an object to extract a result value in 338 | a block passed to `GetResult.apply`. 339 | 340 | ```scala 341 | implicit val getEntryResult = GetResult { r => Entry( 342 | r.column("entry_id"), 343 | r.column("url") 344 | ) } 345 | ``` 346 | 347 | It also requires `getresult.AutoUnwrapOption` to extract a column 348 | value of non-`Option[_]` type. This also available both as a trait 349 | and an object. 350 | 351 | You can use this feature without the extended SQL interpolation. 352 | 353 | ### Mapping columns by name 354 | 355 | As you have already seen, a column value can be extracted by `column` 356 | method. If you want to explicitly specify a return type or the return 357 | type somehow cannot be inferred, you can pass a type parameter to 358 | `column` to specify a return type. 359 | 360 | ```scala 361 | implicit val getEntryResult = getResult { Entry( 362 | column[Long]("entry_id"), 363 | column[String]("url") 364 | ) } 365 | ``` 366 | 367 | ### Mapping columns by position 368 | 369 | You can also extract the result by the index of a column. 370 | 371 | ```scala 372 | implicit val getEntryResult = getResult { Entry( 373 | column(1), 374 | column(2) 375 | ) } 376 | ``` 377 | 378 | If you prefer the way of `<<` method in Slick's `GetResult`, it is 379 | also available in our `GetResult`. 380 | 381 | ```scala 382 | implicit val getEntryResult = getResult { Entry(<<, <<) } 383 | ``` 384 | 385 | `< false }, 50 | pomExtra := ( 51 | https://github.com/tarao/slick-jdbc-extension-scala 52 | 53 | 54 | MIT License 55 | http://www.opensource.org/licenses/mit-license.php 56 | repo 57 | 58 | 59 | 60 | git@github.com:tarao/slick-jdbc-extension-scala.git 61 | scm:git:git@github.com:tarao/slick-jdbc-extension-scala.git 62 | 63 | 64 | 65 | tarao 66 | INA Lintaro 67 | https://github.com/tarao/ 68 | 69 | ) 70 | ) 71 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.6 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1") 2 | 3 | addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") 4 | 5 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.21") 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/main/scala/com/github/tarao/slickjdbc/getresult/GetResult.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | package getresult 4 | 5 | import scala.util.DynamicVariable 6 | import scala.annotation.implicitNotFound 7 | import slick.jdbc.{GetResult => GR, PositionedResult} 8 | import java.sql.{ResultSet} 9 | 10 | trait GetResult { 11 | private val positionedResult = new DynamicVariable[PositionedResult](null) 12 | private[getresult] def positionedResultValue = 13 | Option(positionedResult.value) getOrElse { 14 | throw new RuntimeException("Column access must be in getResult method") 15 | } 16 | 17 | def getResult[T](block: => T): GR[T] = 18 | GR[T] { r => positionedResult.withValue(r) { block } } 19 | 20 | def column[T](index: Int)(implicit 21 | check: CheckGetter[T], 22 | binder: TypeBinder[T] 23 | ): T = binder(positionedResultValue.rs, index) 24 | def column[T](field: String)(implicit 25 | check: CheckGetter[T], 26 | binder: TypeBinder[T] 27 | ): T = binder(positionedResultValue.rs, field) 28 | def skip = { positionedResultValue.skip; this } 29 | def <<[T](implicit 30 | check: CheckGetter[T], 31 | binder: TypeBinder[T] 32 | ): T = column[T](positionedResultValue.skip.currentPos) 33 | def < T): GR[T] = 40 | GR[T] { r => block(new GetResult { 41 | override def positionedResultValue = r 42 | }) } 43 | } 44 | 45 | // $COVERAGE-OFF$ 46 | 47 | @implicitNotFound(msg = "No conversion rule for type ${T}\n" + 48 | "[NOTE] You need an implicit of getresult.TypeBinder[${T}] to convert the result.") 49 | sealed trait CheckGetter[+T] 50 | object CheckGetter { 51 | implicit def valid[T](implicit binder: TypeBinder[T]): CheckGetter[T] = 52 | new CheckGetter[T] {} 53 | } 54 | 55 | // $COVERAGE-ON$ 56 | 57 | trait TypeBinder[+T] { 58 | def apply(rs: ResultSet, index: Int): T 59 | def apply(rs: ResultSet, field: String): T 60 | def map[S](f: T => S): TypeBinder[S] = new TypeBinder[S] { 61 | def apply(rs: ResultSet, index: Int): S = 62 | f(TypeBinder.this.apply(rs, index)) 63 | def apply(rs: ResultSet, field: String): S = 64 | f(TypeBinder.this.apply(rs, field)) 65 | } 66 | } 67 | 68 | object TypeBinder { 69 | type Get[X, R] = (ResultSet, X) => R 70 | def apply[T](byIndex: Get[Int, T])(byField: Get[String, T]): TypeBinder[T] = 71 | new TypeBinder[T] { 72 | def apply(rs: ResultSet, index: Int): T = byIndex(rs, index) 73 | def apply(rs: ResultSet, field: String): T = byField(rs, field) 74 | } 75 | 76 | val any: TypeBinder[Any] = 77 | TypeBinder[Any](_ getObject _)(_ getObject _) 78 | 79 | implicit val javaBoolean: TypeBinder[Option[java.lang.Boolean]] = any.map { 80 | case b if b == null => None 81 | case b: java.lang.Boolean => Some(b) 82 | case s: String => Some({ 83 | try s.toInt != 0 84 | catch { case e: NumberFormatException => !s.isEmpty } 85 | }.asInstanceOf[java.lang.Boolean]) 86 | case n: Number => Some((n.intValue != 0).asInstanceOf[java.lang.Boolean]) 87 | case v => Some((v != 0).asInstanceOf[java.lang.Boolean]) 88 | } 89 | implicit val scalaBoolean: TypeBinder[Option[Boolean]] = 90 | javaBoolean.map(_.map(_.asInstanceOf[Boolean])) 91 | 92 | def javaNumber[T](valueOf: String => T): TypeBinder[Option[T]] = 93 | any.map { 94 | case v if v == null => None 95 | case v => try { 96 | Some(valueOf(v.toString)) 97 | } catch { case e: NumberFormatException => None } 98 | } 99 | def javaFixedNumber[T]( 100 | to: Number => T, 101 | valueOf: String => T 102 | ): TypeBinder[Option[T]] = any.map { 103 | case v if v == null => None 104 | case v: Number => Some(to(v)) 105 | case v => try { 106 | Some(valueOf(v.toString)) 107 | } catch { case e: NumberFormatException => None } 108 | } 109 | 110 | implicit val javaByte: TypeBinder[Option[java.lang.Byte]] = 111 | javaFixedNumber({ n => n.byteValue }, { s => java.lang.Byte.valueOf(s) }) 112 | implicit val scalaByte: TypeBinder[Option[Byte]] = 113 | javaByte.map(_.map(_.asInstanceOf[Byte])) 114 | implicit val javaShort: TypeBinder[Option[java.lang.Short]] = 115 | javaFixedNumber({ n => n.shortValue }, { s => java.lang.Short.valueOf(s) }) 116 | implicit val scalaShort: TypeBinder[Option[Short]] = 117 | javaShort.map(_.map(_.asInstanceOf[Short])) 118 | implicit val javaInt: TypeBinder[Option[java.lang.Integer]] = 119 | javaFixedNumber({ n => n.intValue }, { s => java.lang.Integer.valueOf(s) }) 120 | implicit val scalaInt: TypeBinder[Option[Int]] = 121 | javaInt.map(_.map(_.asInstanceOf[Int])) 122 | implicit val javaLong: TypeBinder[Option[java.lang.Long]] = 123 | javaFixedNumber({ n => n.longValue }, { s => java.lang.Long.valueOf(s) }) 124 | implicit val scalaLong: TypeBinder[Option[Long]] = 125 | javaLong.map(_.map(_.asInstanceOf[Long])) 126 | implicit val javaDouble: TypeBinder[Option[java.lang.Double]] = 127 | javaNumber { s => java.lang.Double.valueOf(s) } 128 | implicit val scalaDouble: TypeBinder[Option[Double]] = 129 | javaDouble.map(_.map(_.asInstanceOf[Double])) 130 | implicit val javaFloat: TypeBinder[Option[java.lang.Float]] = 131 | javaNumber { s => java.lang.Float.valueOf(s) } 132 | implicit val scalaFloat: TypeBinder[Option[Float]] = 133 | javaFloat.map(_.map(_.asInstanceOf[Float])) 134 | 135 | implicit val javaBigDecimal: TypeBinder[Option[java.math.BigDecimal]] = 136 | TypeBinder(_ getBigDecimal _)(_ getBigDecimal _).map(Option(_)) 137 | implicit val scalaBigDecimal: TypeBinder[Option[BigDecimal]] = 138 | javaBigDecimal.map(_.map(BigDecimal(_))) 139 | 140 | implicit val string: TypeBinder[Option[String]] = 141 | TypeBinder(_ getString _)(_ getString _).map(Option(_)) 142 | implicit val bytes: TypeBinder[Option[Array[Byte]]] = 143 | TypeBinder(_ getBytes _)(_ getBytes _).map(Option(_)) 144 | implicit val characterStream: TypeBinder[Option[java.io.Reader]] = 145 | TypeBinder(_ getCharacterStream _)(_ getCharacterStream _).map(Option(_)) 146 | implicit val binaryStream: TypeBinder[Option[java.io.InputStream]] = 147 | TypeBinder(_ getBinaryStream _)(_ getBinaryStream _).map(Option(_)) 148 | 149 | implicit val blob: TypeBinder[Option[java.sql.Blob]] = 150 | TypeBinder(_ getBlob _)(_ getBlob _).map(Option(_)) 151 | implicit val clob: TypeBinder[Option[java.sql.Clob]] = 152 | TypeBinder(_ getClob _)(_ getClob _).map(Option(_)) 153 | implicit val nClob: TypeBinder[Option[java.sql.NClob]] = 154 | TypeBinder(_ getNClob _)(_ getNClob _).map(Option(_)) 155 | implicit val array: TypeBinder[Option[java.sql.Array]] = 156 | TypeBinder(_ getArray _)(_ getArray _).map(Option(_)) 157 | 158 | implicit val url: TypeBinder[Option[java.net.URL]] = 159 | TypeBinder(_ getURL _)(_ getURL _).map(Option(_)) 160 | implicit val date: TypeBinder[Option[java.sql.Date]] = 161 | TypeBinder(_ getDate _)(_ getDate _).map(Option(_)) 162 | implicit val time: TypeBinder[Option[java.sql.Time]] = 163 | TypeBinder(_ getTime _)(_ getTime _).map(Option(_)) 164 | implicit val timestamp: TypeBinder[Option[java.sql.Timestamp]] = 165 | TypeBinder(_ getTimestamp _)(_ getTimestamp _).map(Option(_)) 166 | implicit val sqlxml: TypeBinder[Option[java.sql.SQLXML]] = 167 | TypeBinder(_ getSQLXML _)(_ getSQLXML _).map(Option(_)) 168 | implicit val ref: TypeBinder[Option[java.sql.Ref]] = 169 | TypeBinder(_ getRef _)(_ getRef _).map(Option(_)) 170 | implicit val rowId: TypeBinder[Option[java.sql.RowId]] = 171 | TypeBinder(_ getRowId _)(_ getRowId _).map(Option(_)) 172 | } 173 | 174 | trait AutoUnwrapOption { 175 | implicit def some[T](implicit 176 | check: IsNotOption[T], // We do this to enable a diagnostic type 177 | // error by CheckGetter. Otherwise an 178 | // implicit expansion of an unknown type 179 | // fails on divergence. 180 | option: TypeBinder[Option[T]] 181 | ): TypeBinder[T] = option.map(_.get) // throws 182 | } 183 | object AutoUnwrapOption extends AutoUnwrapOption 184 | 185 | // $COVERAGE-OFF$ 186 | 187 | sealed trait IsNotOption[+T] 188 | object IsNotOption { 189 | implicit def some[T]: IsNotOption[T] = new IsNotOption[T] {} 190 | implicit def ambig1[T]: IsNotOption[Option[T]] = sys.error("unexpected") 191 | implicit def ambig2[T]: IsNotOption[Option[T]] = sys.error("unexpected") 192 | } 193 | 194 | // $COVERAGE-ON$ 195 | -------------------------------------------------------------------------------- /src/main/scala/com/github/tarao/slickjdbc/interpolation/Interpolation.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | package interpolation 4 | 5 | import java.sql.PreparedStatement 6 | import scala.language.implicitConversions 7 | import slick.SlickException 8 | import slick.jdbc.{ 9 | GetResult, 10 | PositionedParameters, 11 | PositionedResult, 12 | SQLInterpolation => SlickInterpolation, 13 | SetParameter, 14 | StatementInvoker, 15 | StreamingInvokerAction, 16 | TypedParameter 17 | } 18 | import slick.sql.{SqlAction, SqlStreamingAction} 19 | import slick.dbio.{Effect, NoStream} 20 | 21 | trait SQLInterpolation { 22 | implicit def interpolation(s: StringContext): SQLInterpolationImpl = SQLInterpolationImpl(s) 23 | } 24 | object SQLInterpolation extends SQLInterpolation 25 | 26 | case class SQLInterpolationImpl(s: StringContext) extends AnyVal { 27 | import scala.language.experimental.macros 28 | 29 | def sql(params: Any*): SQLActionBuilder = 30 | macro MacroTreeBuilder.sqlImpl 31 | 32 | def sqlu(params: Any*): SqlAction[Int, NoStream, Effect] = 33 | macro MacroTreeBuilder.sqluImpl 34 | } 35 | 36 | trait Literal 37 | class SimpleString(value: String) extends Literal { 38 | override def toString = value 39 | } 40 | case class TableName(name: String) extends SimpleString(name) 41 | 42 | object GetUpdateValue extends GetResult[Int] { 43 | def apply(pr: PositionedResult) = 44 | throw new SlickException("Update statements should not return a ResultSet") 45 | } 46 | 47 | case class SQLActionBuilder(strings: Seq[String], params: Seq[TypedParameter[_]]) { 48 | def as[R](implicit 49 | getResult: GetResult[R], 50 | translators: Iterable[query.Translator] 51 | ): SqlStreamingAction[Vector[R], R, Effect] = { 52 | val (sql, unitPConv) = 53 | SlickInterpolation.parse(strings, params.asInstanceOf[Seq[TypedParameter[Any]]]) 54 | val translatedStatements = List(query.Translator.translate(sql)) 55 | new StreamingInvokerAction[Vector[R], R, Effect] { 56 | def statements = translatedStatements 57 | protected[this] def createInvoker(statements: Iterable[String]) = new StatementInvoker[R] { 58 | val getStatement = statements.head 59 | protected def setParam(st: PreparedStatement) = unitPConv((), new PositionedParameters(st)) 60 | protected def extractValue(rs: PositionedResult): R = getResult(rs) 61 | } 62 | protected[this] def createBuilder = Vector.newBuilder[R] 63 | } 64 | } 65 | 66 | def asUpdate(implicit 67 | translators: Iterable[query.Translator] 68 | ) = as[Int](GetUpdateValue, translators).head 69 | } 70 | -------------------------------------------------------------------------------- /src/main/scala/com/github/tarao/slickjdbc/interpolation/MacroTreeBuilder.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | package interpolation 4 | 5 | import interpolation.{Literal => LiteralParameter} 6 | import scala.reflect.macros.blackbox.Context 7 | 8 | private[interpolation] class MacroTreeBuilder(val c: Context) { 9 | import c.universe._ 10 | import scala.collection.mutable.ListBuffer 11 | import slick.sql.SqlAction 12 | import slick.dbio.{NoStream, Effect} 13 | 14 | def abort(msg: String) = c.abort(c.enclosingPosition, msg) 15 | 16 | // Retrieve string parts of interpolation from caller context 17 | lazy val rawQueryParts: List[String] = { 18 | // match SQLInterpolationImpl(StringContext(strArg: _*)).sql(params: _*) 19 | val Apply(Select(Apply(_, List(Apply(_, strArg))), _), params) = 20 | c.macroApplication 21 | strArg map { 22 | case Literal(Constant(x: String)) => x 23 | case _ => abort("The interpolation must be a string literal") 24 | } 25 | } 26 | 27 | private val NS = q"com.github.tarao.slickjdbc" 28 | private val interpolation = q"$NS.interpolation" 29 | private lazy val ListRejected = 30 | tq"""$interpolation.${TypeName("ListRejected")}""" 31 | private lazy val OptionRejected = 32 | tq"""$interpolation.${TypeName("OptionRejected")}""" 33 | private lazy val EitherRejected = 34 | tq"""$interpolation.${TypeName("EitherRejected")}""" 35 | private lazy val ValidParameter = 36 | tq"""$interpolation.${TypeName("ValidParameter")}""" 37 | private lazy val ValidProduct = 38 | tq"""$interpolation.${TypeName("ValidProduct")}""" 39 | private lazy val ValidRefinedNonEmpty = 40 | tq"""$interpolation.${TypeName("ValidRefinedNonEmpty")}""" 41 | private def ensure(required: Type, base: Tree = ValidParameter) = 42 | q"implicitly[$base[$required]]" 43 | private val ToPlaceholder = 44 | tq"""$interpolation.${TypeName("ToPlaceholder")}""" 45 | private def toPlaceholder(target: Type, base: Tree = ToPlaceholder) = 46 | q"implicitly[$base[$target]]" 47 | 48 | def invokeInterpolation(param: c.Expr[Any]*): Tree = { 49 | val stats = new ListBuffer[Tree] 50 | 51 | // Additional features of SQL interpolation by preprocessing 52 | // string parts and arguments. The interpolation translates to 53 | // another (expanded) interpolation call of 54 | // `ActionBasedSQLInterpolation`. 55 | // 56 | // [Embedding literals] 57 | // 58 | // A parameter of type `Literal` is embedded as a literal string 59 | // by using "#" expansion. 60 | // 61 | // val literal = new Literal { def toString = "string" } 62 | // sql"some text with a ${literal} value" 63 | // ~> SQLInterpolationImpl( 64 | // StringContext("some text with a ", " value") 65 | // ).sql(literal) 66 | // ~> ActionBasedSQLInterpolation( 67 | // StringContext("some text with a #", " value") 68 | // ).sql(literal) 69 | // => SQLActionBuilder(Seq("some text with a string value"), ...) 70 | // 71 | // [Embedding non-empty lists] 72 | // 73 | // A parameter of type `NonEmpty[Any]` is embedded with repeated 74 | // "?"s. "?"s (except the last one) are inserted as a literal 75 | // string parameter not as a literal string part of 76 | // `StringContext` since the number of elements is not known at 77 | // compile time. 78 | // 79 | // val list = NonEmpty(1, 2, 3) 80 | // sql"some text with a ${list} value" 81 | // ~> SQLInterpolationImpl( 82 | // StringContext("some text with a (#", "", ")#", " value") 83 | // ).sql(new Placeholders(list), list, "") 84 | // ~> ActionBasedSQLInterpolation( 85 | // StringContext("some text with a (#", "", ")#", " value") 86 | // ).sql(new Placeholders(list), list, "") 87 | // => ActionBasedSQLInterpolation( 88 | // StringContext("some text with a (#", "", ")#", " value") 89 | // ).sql("?, ?, ", list, "") 90 | // => SQLActionBuilder(Seq("some text with a (?, ?, ?) value"), ...) 91 | // 92 | // Note that the third "?" is inserted by a conversion of argument 93 | // `list`. 94 | val queryParts = new ListBuffer[Tree] 95 | val params = new ListBuffer[c.Expr[Any]] 96 | def pushLiteral(literal: String) = { 97 | params.append(c.Expr(q""" ${""} """)) 98 | queryParts.append(q""" ${literal + "#"} """) 99 | } 100 | def pushLiteralValue(expr: c.Expr[Any]) = { 101 | params.append(c.Expr(q"""slick.jdbc.TypedParameter.typedParameter($expr)(new ${interpolation}.SetLiteral)""")) 102 | } 103 | def pushValue(expr: c.Expr[Any]) = { 104 | params.append(c.Expr(q"""slick.jdbc.TypedParameter.typedParameter($expr)""")) 105 | } 106 | def mayCompleteParen(param: c.Expr[Any], s: String)(block: => Unit) = { 107 | if (!s.matches("""(?s).*\(\s*""")) { 108 | pushLiteralValue(c.Expr(q""" ${toPlaceholder(param.actualType)}.open """)) 109 | queryParts.append(q""" ${"#"} """) 110 | block 111 | pushLiteralValue(c.Expr(q""" ${toPlaceholder(param.actualType)}.close """)) 112 | queryParts.append(q""" ${"#"} """) 113 | } else block 114 | } 115 | param.toList.iterator.zip(rawQueryParts.iterator).foreach { zipped => 116 | val (param, s, literal) = zipped match { case (param, s) => { 117 | val literal = s.reverseIterator.takeWhile(_ == '#').length % 2 == 1 118 | if (param.actualType <:< typeOf[LiteralParameter]) 119 | (param, s + { if (literal) "" else "#" }, true) 120 | else (param, s, literal) 121 | } } 122 | if (!literal) { 123 | pushLiteral(s) 124 | 125 | mayCompleteParen(param, s) { 126 | // for "?, ?, ?, ..." except the last one 127 | pushLiteralValue(c.Expr(q""" 128 | ${toPlaceholder(param.actualType)} 129 | .apply(${param}) 130 | .toTopLevelString 131 | """)) 132 | queryParts.append(q""" ${"#"} """) 133 | 134 | // for the last "?" (inserted by ActionBasedSQLInterpolation) 135 | pushValue(param) 136 | queryParts.append(q""" ${""} """) 137 | } 138 | } else { 139 | pushLiteralValue(param) 140 | queryParts.append(q"$s") 141 | } 142 | 143 | if (!literal) { 144 | // Insert parameter type checker for a fine type error message. 145 | 146 | // The order is significant since there can be a type matches 147 | // with multiple conditions for example an 148 | // Option[NonEmpty[Any]] is also a Product. 149 | 150 | stats.append(ensure(param.actualType, ValidRefinedNonEmpty)) 151 | stats.append(ensure(param.actualType, ListRejected)) 152 | 153 | param.actualType.foreach { t => 154 | if (t <:< typeOf[Any]) { 155 | stats.append(ensure(t, OptionRejected)) 156 | stats.append(ensure(t, EitherRejected)) 157 | } 158 | } 159 | 160 | stats.append(ensure(param.actualType, ValidProduct)) 161 | stats.append(ensure(param.actualType, ValidParameter)) 162 | } 163 | } 164 | queryParts.append(q"${rawQueryParts.last}") 165 | 166 | stats.append(q"""new $interpolation.SQLActionBuilder(Seq(..$queryParts), Seq(..$params))""") 167 | q"{ ..$stats }" 168 | 169 | } 170 | 171 | def sqlImpl(params: c.Expr[Any]*): c.Expr[SQLActionBuilder] = 172 | c.Expr(invokeInterpolation(params: _*)) 173 | 174 | def sqluImpl(params: c.Expr[Any]*): c.Expr[SqlAction[Int, NoStream, Effect]] = 175 | c.Expr(q""" ${invokeInterpolation(params: _*)}.asUpdate """) 176 | } 177 | -------------------------------------------------------------------------------- /src/main/scala/com/github/tarao/slickjdbc/interpolation/Placeholder.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | package interpolation 4 | 5 | class Placeholder extends Literal { 6 | import Placeholder.{stripParen, dropLast} 7 | def toSeq: Seq[Placeholder] = Seq(this) 8 | def toTopLevelString: String = dropLast(stripParen(toString)) 9 | override def toString: String = "?" 10 | } 11 | object Placeholder { 12 | private def stripParen(str: String) = str.stripPrefix("(").stripSuffix(")") 13 | private def dropLast(str: String) = str.stripSuffix("?") 14 | 15 | def apply(): Placeholder = new Placeholder 16 | def repeat(n: Int): Seq[Placeholder] = (1 to n).map(_ => apply()) 17 | 18 | class Nested(children: Placeholder*) extends Placeholder { 19 | override def toSeq: Seq[Placeholder] = children 20 | override def toString: String = 21 | s"""(${stripParen(children.map(_.toString).mkString(", "))})""" 22 | } 23 | object Nested { 24 | def apply(children: Placeholder*): Nested = new Nested(children: _*) 25 | def apply(n: Int): Nested = new Nested(Placeholder.repeat(n): _*) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/scala/com/github/tarao/slickjdbc/interpolation/SetParameter.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | package interpolation 4 | 5 | import eu.timepit.refined.api.RefType 6 | import eu.timepit.refined.collection.NonEmpty 7 | import scala.annotation.implicitNotFound 8 | import scala.language.higherKinds 9 | import scala.language.implicitConversions 10 | import slick.jdbc.{SetParameter => SP, PositionedParameters} 11 | 12 | trait ListParameter { 13 | @inline implicit def createSetNonEmptyList[A, L[X] <: Iterable[X], F[_, _]](implicit 14 | c: SP[A], 15 | rt: RefType[F] 16 | ): SP[F[L[A], NonEmpty]] = 17 | new SetNonEmptyList[A, L, F, F[L[A], NonEmpty]](c, rt) 18 | 19 | @inline implicit def nonEmptyListToPlaceholder[A, L[X] <: Iterable[X], F[_, _]](implicit 20 | p: ToPlaceholder[A], 21 | rt: RefType[F] 22 | ): ToPlaceholder[F[L[A], NonEmpty]] = 23 | new ToPlaceholder.FromNonEmptyList[A, L, F, F[L[A], NonEmpty]](p, rt) 24 | } 25 | object ListParameter extends ListParameter 26 | 27 | trait ProductParameter { 28 | @inline implicit def createSetProduct[T](implicit 29 | check1: T <:< Product, 30 | check2: IsNotTuple[T] 31 | ): SP[T] = new SetProduct[T] 32 | 33 | @inline implicit def productToPlaceholder[T](implicit 34 | check1: T <:< Product, 35 | check2: IsNotTuple[T] 36 | ): ToPlaceholder[T] = new ToPlaceholder.FromProduct[T] 37 | } 38 | object ProductParameter extends ProductParameter 39 | 40 | trait CompoundParameter extends ListParameter with ProductParameter 41 | object CompoundParameter extends CompoundParameter 42 | 43 | /** SetParameter for non-empty list types. */ 44 | class SetNonEmptyList[A, L[X] <: Iterable[X], F[_, _], -T <: F[L[A], NonEmpty]](val c: SP[A], rt: RefType[F]) extends SP[T] { 45 | def apply(param: T, pp: PositionedParameters): Unit = { 46 | rt.unwrap(param).foreach(item => c.asInstanceOf[SP[Any]](item, pp)) 47 | } 48 | } 49 | 50 | /** SetParameter for product types especially for case classes. */ 51 | class SetProduct[-T](implicit product: T <:< Product) extends SP[T] { 52 | def apply(prod: T, pp: PositionedParameters): Unit = 53 | for (v <- product(prod).productIterator) v match { 54 | case p: Product => new SetProduct[Product].apply(p, pp) 55 | case v => SP.SetSimpleProduct(Tuple1(v), pp) 56 | } 57 | } 58 | 59 | class SetLiteral[-T] extends SP[T] { 60 | def apply(lit: T, pp: PositionedParameters): Unit = 61 | SP.SetString(lit.toString(), pp) 62 | } 63 | 64 | // $COVERAGE-OFF$ 65 | 66 | @implicitNotFound(msg = "Unsupported parameter type: ${T}.\n" + 67 | "[NOTE] You need an implicit of slick.jdbc.SetParameter[${T}] to pass a value of the type.") 68 | sealed trait ValidParameter[-T] 69 | object ValidParameter { 70 | implicit def valid[T](implicit c: SP[T]): ValidParameter[T] = 71 | new ValidParameter[T] {} 72 | } 73 | 74 | @implicitNotFound(msg = "A product ${T} is passed.\n" + 75 | "[NOTE] Use interpolation.CompoundParameter trait to enable passing a product.") 76 | sealed trait ValidProduct[-T] 77 | object ValidProduct { 78 | implicit def valid1[T](implicit 79 | c: SP[T], 80 | product: T <:< Product 81 | ): ValidProduct[T] = new ValidProduct[T] {} 82 | 83 | implicit def valid2[T](implicit check: IsNotProduct[T]): ValidProduct[T] = 84 | new ValidProduct[T] {} 85 | } 86 | 87 | @implicitNotFound(msg = "A non-empty list is passed.\n" + 88 | "[NOTE] Use interpolation.CompoundParameter trait to enable passing a non-empty list.") 89 | sealed trait ValidRefinedNonEmpty[-T] 90 | object ValidRefinedNonEmpty { 91 | implicit def valid1[A, L[X] <: Iterable[X], F[_, _]](implicit 92 | c: SP[F[L[A], NonEmpty]], 93 | rt: RefType[F] 94 | ): ValidRefinedNonEmpty[F[L[A], NonEmpty]] = 95 | new ValidRefinedNonEmpty[F[L[A], NonEmpty]] {} 96 | 97 | implicit def valid2[T](implicit 98 | check: IsNotRefinedNonEmpty[T] 99 | ): ValidRefinedNonEmpty[T] = new ValidRefinedNonEmpty[T] {} 100 | } 101 | 102 | @implicitNotFound(msg = "Illegal parameter type: ${T}.\n" + 103 | "[NOTE] A list is not allowed since it may be empty and breaks the query.\n" + 104 | "[NOTE] Pass a ${T} Refind NonEmpty if you know that it is not empty.") 105 | sealed trait ListRejected[-T] 106 | object ListRejected { 107 | implicit def valid[T](implicit check: IsNotList[T]): ListRejected[T] = 108 | new ListRejected[T] {} 109 | } 110 | 111 | @implicitNotFound(msg = "Illegal parameter type: ${T}\n" + 112 | "[NOTE] An option is not allowed since it may be none and breaks the query.\n" + 113 | "[NOTE] Break it into Some(_) or None to confirm that it has a value.") 114 | sealed trait OptionRejected[-T] 115 | object OptionRejected { 116 | implicit def valid[T](implicit check: IsNotOption[T]): OptionRejected[T] = 117 | new OptionRejected[T] {} 118 | } 119 | 120 | @implicitNotFound(msg = "Illegal parameter type: ${T}\n" + 121 | "[NOTE] Break it into Left(_) or Right(_) to confirm that it can be embedded into the query.") 122 | sealed trait EitherRejected[-T] 123 | object EitherRejected { 124 | implicit def valid[T](implicit check: IsNotEither[T]): EitherRejected[T] = 125 | new EitherRejected[T] {} 126 | } 127 | 128 | sealed trait IsNotProduct[-T] 129 | object IsNotProduct { 130 | implicit def valid[T]: IsNotProduct[T] = new IsNotProduct[T] {} 131 | implicit def ambig1: IsNotProduct[Product] = sys.error("unexpected") 132 | implicit def ambig2: IsNotProduct[Product] = sys.error("unexpected") 133 | } 134 | 135 | sealed trait IsNotOption[-T] 136 | object IsNotOption { 137 | implicit def valid[T]: IsNotOption[T] = new IsNotOption[T] {} 138 | implicit def ambig1[S]: IsNotOption[Option[S]] = sys.error("unexpected") 139 | implicit def ambig2[S]: IsNotOption[Option[S]] = sys.error("unexpected") 140 | } 141 | 142 | sealed trait IsNotTuple[-T] 143 | object IsNotTuple { 144 | implicit def valid[T]: IsNotTuple[T] = new IsNotTuple[T] {} 145 | implicit def ambig1[S](implicit tp: IsTuple[S]): IsNotTuple[S] = 146 | sys.error("unexpected") 147 | implicit def ambig2[S](implicit tp: IsTuple[S]): IsNotTuple[S] = 148 | sys.error("unexpected") 149 | } 150 | 151 | sealed trait IsNotEither[-T] 152 | object IsNotEither { 153 | implicit def valid[T]: IsNotEither[T] = new IsNotEither[T] {} 154 | implicit def ambig1[S](implicit either: S <:< Either[_, _]): IsNotEither[S] = 155 | sys.error("unexpected") 156 | implicit def ambig2[S](implicit either: S <:< Either[_, _]): IsNotEither[S] = 157 | sys.error("unexpected") 158 | } 159 | 160 | sealed trait IsNotList[-T] 161 | object IsNotList { 162 | implicit def valid[T]: IsNotList[T] = new IsNotList[T] {} 163 | implicit def ambig1[S]: IsNotList[Iterable[S]] = sys.error("unexpected") 164 | implicit def ambig2[S]: IsNotList[Iterable[S]] = sys.error("unexpected") 165 | } 166 | 167 | sealed trait IsNotRefinedNonEmpty[-T] 168 | object IsNotRefinedNonEmpty { 169 | implicit def valid[T]: IsNotRefinedNonEmpty[T] = 170 | new IsNotRefinedNonEmpty[T] {} 171 | 172 | implicit def ambig1[A, L[X] <: Iterable[X], F[_, _]](implicit 173 | rt: RefType[F] 174 | ): IsNotRefinedNonEmpty[F[L[A], NonEmpty]] = sys.error("unexpected") 175 | implicit def ambig2[A, L[X] <: Iterable[X], F[_, _]](implicit 176 | rt: RefType[F] 177 | ): IsNotRefinedNonEmpty[F[L[A], NonEmpty]] = sys.error("unexpected") 178 | } 179 | 180 | sealed trait IsTuple[-T] 181 | object IsTuple { 182 | implicit def tuple2[T1, T2]: IsTuple[(T1, T2)] = 183 | new IsTuple[(T1, T2)] {} 184 | implicit def tuple3[T1, T2, T3]: IsTuple[(T1, T2, T3)] = 185 | new IsTuple[(T1, T2, T3)] {} 186 | implicit def tuple4[T1, T2, T3, T4]: IsTuple[(T1, T2, T3, T4)] = 187 | new IsTuple[(T1, T2, T3, T4)] {} 188 | implicit def tuple5[T1, T2, T3, T4, T5]: IsTuple[(T1, T2, T3, T4, T5)] = 189 | new IsTuple[(T1, T2, T3, T4, T5)] {} 190 | implicit def tuple6[T1, T2, T3, T4, T5, T6]: IsTuple[(T1, T2, T3, T4, T5, T6)] = 191 | new IsTuple[(T1, T2, T3, T4, T5, T6)] {} 192 | implicit def tuple7[T1, T2, T3, T4, T5, T6, T7]: IsTuple[(T1, T2, T3, T4, T5, T6, T7)] = 193 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7)] {} 194 | implicit def tuple8[T1, T2, T3, T4, T5, T6, T7, T8]: IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8)] = 195 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8)] {} 196 | implicit def tuple9[T1, T2, T3, T4, T5, T6, T7, T8, T9]: IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9)] = 197 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9)] {} 198 | implicit def tuple10[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]: IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10)] = 199 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10)] {} 200 | implicit def tuple11[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11]: IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11)] = 201 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11)] {} 202 | implicit def tuple12[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12]: IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12)] = 203 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12)] {} 204 | implicit def tuple13[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13]: IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13)] = 205 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13)] {} 206 | implicit def tuple14[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14]: IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14)] = 207 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14)] {} 208 | implicit def tuple15[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15]: IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15)] = 209 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15)] {} 210 | implicit def tuple16[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16]: IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16)] = 211 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16)] {} 212 | implicit def tuple17[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17]: IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17)] = 213 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17)] {} 214 | implicit def tuple18[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18]: IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18)] = 215 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18)] {} 216 | implicit def tuple19[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19]: IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19)] = 217 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19)] {} 218 | implicit def tuple20[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20]: IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20)] = 219 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20)] {} 220 | implicit def tuple21[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21]: IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21)] = 221 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21)] {} 222 | implicit def tuple22[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22]: IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22)] = 223 | new IsTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22)] {} 224 | } 225 | 226 | // $COVERAGE-ON$ 227 | -------------------------------------------------------------------------------- /src/main/scala/com/github/tarao/slickjdbc/interpolation/ToPlaceholder.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | package interpolation 4 | 5 | import eu.timepit.refined.api.RefType 6 | import eu.timepit.refined.collection.NonEmpty 7 | import scala.language.higherKinds 8 | 9 | trait ToPlaceholder[-T] { 10 | def apply(value: T): Placeholder 11 | def open: String = "" 12 | def close: String = "" 13 | } 14 | object ToPlaceholder { 15 | trait Compound[-T] extends ToPlaceholder[T] { 16 | override def open: String = "(" 17 | override def close: String = ")" 18 | } 19 | 20 | class FromProduct[-T](implicit product: T <:< Product) 21 | extends Compound[T] { 22 | def apply(value: T): Placeholder = { 23 | def rec(v: Any): Placeholder = v match { 24 | case s: Tuple1[_] => 25 | Placeholder.Nested(1) 26 | case p: Product if p.productArity <= 0 => 27 | throw new java.sql.SQLException("No value to bind for " + p) 28 | case p: Product if p.productArity == 1 => 29 | p.productIterator.map(rec _).toSeq.head 30 | case p: Product => 31 | Placeholder.Nested(p.productIterator.flatMap(rec(_).toSeq).toSeq: _*) 32 | case _ => Placeholder() 33 | } 34 | rec(value) 35 | } 36 | } 37 | 38 | class FromNonEmptyList[A, L[X] <: Iterable[X], F[_, _], -T <: F[L[A], NonEmpty]]( 39 | p: ToPlaceholder[A], 40 | rt: RefType[F] 41 | ) extends Compound[T] { 42 | def apply(value: T): Placeholder = 43 | Placeholder.Nested(rt.unwrap(value).map(p.apply _).toSeq: _*) 44 | } 45 | 46 | class FromTuple[-T <: Product](children: ToPlaceholder[_]*) 47 | extends Compound[T] { 48 | def apply(value: T): Placeholder = Placeholder.Nested( 49 | children.iterator.zip(value.productIterator).map { pair => 50 | pair._1.asInstanceOf[ToPlaceholder[Any]].apply(pair._2) 51 | }.toSeq: _* 52 | ) 53 | } 54 | 55 | @inline implicit def anyToPlaceholder[T]: ToPlaceholder[T] = 56 | new ToPlaceholder[T] { 57 | def apply(value: T): Placeholder = Placeholder() 58 | } 59 | 60 | @inline implicit def tuple1ToPlaceholder[T1](implicit 61 | p1: ToPlaceholder[T1] 62 | ): ToPlaceholder[Tuple1[T1]] = 63 | new FromTuple[Tuple1[T1]](p1) 64 | 65 | @inline implicit def tuple2ToPlaceholder[T1, T2](implicit 66 | p1: ToPlaceholder[T1], 67 | p2: ToPlaceholder[T2] 68 | ): ToPlaceholder[(T1, T2)] = 69 | new FromTuple[(T1, T2)](p1, p2) 70 | 71 | @inline implicit def tuple3ToPlaceholder[T1, T2, T3, T5](implicit 72 | p1: ToPlaceholder[T1], 73 | p2: ToPlaceholder[T2], 74 | p3: ToPlaceholder[T3] 75 | ): ToPlaceholder[(T1, T2, T3)] = 76 | new FromTuple[(T1, T2, T3)](p1, p2, p3) 77 | 78 | @inline implicit def tuple4ToPlaceholder[T1, T2, T3, T4](implicit 79 | p1: ToPlaceholder[T1], 80 | p2: ToPlaceholder[T2], 81 | p3: ToPlaceholder[T3], 82 | p4: ToPlaceholder[T4] 83 | ): ToPlaceholder[(T1, T2, T3, T4)] = 84 | new FromTuple[(T1, T2, T3, T4)](p1, p2, p3, p4) 85 | 86 | @inline implicit def tuple5ToPlaceholder[T1, T2, T3, T4, T5](implicit 87 | p1: ToPlaceholder[T1], 88 | p2: ToPlaceholder[T2], 89 | p3: ToPlaceholder[T3], 90 | p4: ToPlaceholder[T4], 91 | p5: ToPlaceholder[T5] 92 | ): ToPlaceholder[(T1, T2, T3, T4, T5)] = 93 | new FromTuple[(T1, T2, T3, T4, T5)](p1, p2, p3, p4, p5) 94 | 95 | @inline implicit def tuple6ToPlaceholder[T1, T2, T3, T4, T5, T6](implicit 96 | p1: ToPlaceholder[T1], 97 | p2: ToPlaceholder[T2], 98 | p3: ToPlaceholder[T3], 99 | p4: ToPlaceholder[T4], 100 | p5: ToPlaceholder[T5], 101 | p6: ToPlaceholder[T6] 102 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6)] = 103 | new FromTuple[(T1, T2, T3, T4, T5, T6)](p1, p2, p3, p4, p5, p6) 104 | 105 | @inline implicit def tuple7ToPlaceholder[T1, T2, T3, T4, T5, T6, T7](implicit 106 | p1: ToPlaceholder[T1], 107 | p2: ToPlaceholder[T2], 108 | p3: ToPlaceholder[T3], 109 | p4: ToPlaceholder[T4], 110 | p5: ToPlaceholder[T5], 111 | p6: ToPlaceholder[T6], 112 | p7: ToPlaceholder[T7] 113 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7)] = 114 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7)](p1, p2, p3, p4, p5, p6, p7) 115 | 116 | @inline implicit def tuple8ToPlaceholder[T1, T2, T3, T4, T5, T6, T7, T8](implicit 117 | p1: ToPlaceholder[T1], 118 | p2: ToPlaceholder[T2], 119 | p3: ToPlaceholder[T3], 120 | p4: ToPlaceholder[T4], 121 | p5: ToPlaceholder[T5], 122 | p6: ToPlaceholder[T6], 123 | p7: ToPlaceholder[T7], 124 | p8: ToPlaceholder[T8] 125 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7, T8)] = 126 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7, T8)](p1, p2, p3, p4, p5, p6, p7, p8) 127 | 128 | @inline implicit def tuple9ToPlaceholder[T1, T2, T3, T4, T5, T6, T7, T8, T9](implicit 129 | p1: ToPlaceholder[T1], 130 | p2: ToPlaceholder[T2], 131 | p3: ToPlaceholder[T3], 132 | p4: ToPlaceholder[T4], 133 | p5: ToPlaceholder[T5], 134 | p6: ToPlaceholder[T6], 135 | p7: ToPlaceholder[T7], 136 | p8: ToPlaceholder[T8], 137 | p9: ToPlaceholder[T9] 138 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7, T8, T9)] = 139 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9)](p1, p2, p3, p4, p5, p6, p7, p8, p9) 140 | 141 | @inline implicit def tuple10ToPlaceholder[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10](implicit 142 | p1: ToPlaceholder[T1], 143 | p2: ToPlaceholder[T2], 144 | p3: ToPlaceholder[T3], 145 | p4: ToPlaceholder[T4], 146 | p5: ToPlaceholder[T5], 147 | p6: ToPlaceholder[T6], 148 | p7: ToPlaceholder[T7], 149 | p8: ToPlaceholder[T8], 150 | p9: ToPlaceholder[T9], 151 | p10: ToPlaceholder[T10] 152 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10)] = 153 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10)](p1, p2, p3, p4, p5, p6, p7, p8, p9, p10) 154 | 155 | @inline implicit def tuple11ToPlaceholder[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11](implicit 156 | p1: ToPlaceholder[T1], 157 | p2: ToPlaceholder[T2], 158 | p3: ToPlaceholder[T3], 159 | p4: ToPlaceholder[T4], 160 | p5: ToPlaceholder[T5], 161 | p6: ToPlaceholder[T6], 162 | p7: ToPlaceholder[T7], 163 | p8: ToPlaceholder[T8], 164 | p9: ToPlaceholder[T9], 165 | p10: ToPlaceholder[T10], 166 | p11: ToPlaceholder[T11] 167 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11)] = 168 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11)](p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11) 169 | 170 | @inline implicit def tuple12ToPlaceholder[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12](implicit 171 | p1: ToPlaceholder[T1], 172 | p2: ToPlaceholder[T2], 173 | p3: ToPlaceholder[T3], 174 | p4: ToPlaceholder[T4], 175 | p5: ToPlaceholder[T5], 176 | p6: ToPlaceholder[T6], 177 | p7: ToPlaceholder[T7], 178 | p8: ToPlaceholder[T8], 179 | p9: ToPlaceholder[T9], 180 | p10: ToPlaceholder[T10], 181 | p11: ToPlaceholder[T11], 182 | p12: ToPlaceholder[T12] 183 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12)] = 184 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12)](p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12) 185 | 186 | @inline implicit def tuple13ToPlaceholder[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13](implicit 187 | p1: ToPlaceholder[T1], 188 | p2: ToPlaceholder[T2], 189 | p3: ToPlaceholder[T3], 190 | p4: ToPlaceholder[T4], 191 | p5: ToPlaceholder[T5], 192 | p6: ToPlaceholder[T6], 193 | p7: ToPlaceholder[T7], 194 | p8: ToPlaceholder[T8], 195 | p9: ToPlaceholder[T9], 196 | p10: ToPlaceholder[T10], 197 | p11: ToPlaceholder[T11], 198 | p12: ToPlaceholder[T12], 199 | p13: ToPlaceholder[T13] 200 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13)] = 201 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13)](p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13) 202 | 203 | @inline implicit def tuple14ToPlaceholder[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14](implicit 204 | p1: ToPlaceholder[T1], 205 | p2: ToPlaceholder[T2], 206 | p3: ToPlaceholder[T3], 207 | p4: ToPlaceholder[T4], 208 | p5: ToPlaceholder[T5], 209 | p6: ToPlaceholder[T6], 210 | p7: ToPlaceholder[T7], 211 | p8: ToPlaceholder[T8], 212 | p9: ToPlaceholder[T9], 213 | p10: ToPlaceholder[T10], 214 | p11: ToPlaceholder[T11], 215 | p12: ToPlaceholder[T12], 216 | p13: ToPlaceholder[T13], 217 | p14: ToPlaceholder[T14] 218 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14)] = 219 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14)](p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14) 220 | 221 | @inline implicit def tuple15ToPlaceholder[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15](implicit 222 | p1: ToPlaceholder[T1], 223 | p2: ToPlaceholder[T2], 224 | p3: ToPlaceholder[T3], 225 | p4: ToPlaceholder[T4], 226 | p5: ToPlaceholder[T5], 227 | p6: ToPlaceholder[T6], 228 | p7: ToPlaceholder[T7], 229 | p8: ToPlaceholder[T8], 230 | p9: ToPlaceholder[T9], 231 | p10: ToPlaceholder[T10], 232 | p11: ToPlaceholder[T11], 233 | p12: ToPlaceholder[T12], 234 | p13: ToPlaceholder[T13], 235 | p14: ToPlaceholder[T14], 236 | p15: ToPlaceholder[T15] 237 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15)] = 238 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15)](p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15) 239 | 240 | @inline implicit def tuple16ToPlaceholder[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16](implicit 241 | p1: ToPlaceholder[T1], 242 | p2: ToPlaceholder[T2], 243 | p3: ToPlaceholder[T3], 244 | p4: ToPlaceholder[T4], 245 | p5: ToPlaceholder[T5], 246 | p6: ToPlaceholder[T6], 247 | p7: ToPlaceholder[T7], 248 | p8: ToPlaceholder[T8], 249 | p9: ToPlaceholder[T9], 250 | p10: ToPlaceholder[T10], 251 | p11: ToPlaceholder[T11], 252 | p12: ToPlaceholder[T12], 253 | p13: ToPlaceholder[T13], 254 | p14: ToPlaceholder[T14], 255 | p15: ToPlaceholder[T15], 256 | p16: ToPlaceholder[T16] 257 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16)] = 258 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16)](p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16) 259 | 260 | @inline implicit def tuple17ToPlaceholder[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17](implicit 261 | p1: ToPlaceholder[T1], 262 | p2: ToPlaceholder[T2], 263 | p3: ToPlaceholder[T3], 264 | p4: ToPlaceholder[T4], 265 | p5: ToPlaceholder[T5], 266 | p6: ToPlaceholder[T6], 267 | p7: ToPlaceholder[T7], 268 | p8: ToPlaceholder[T8], 269 | p9: ToPlaceholder[T9], 270 | p10: ToPlaceholder[T10], 271 | p11: ToPlaceholder[T11], 272 | p12: ToPlaceholder[T12], 273 | p13: ToPlaceholder[T13], 274 | p14: ToPlaceholder[T14], 275 | p15: ToPlaceholder[T15], 276 | p16: ToPlaceholder[T16], 277 | p17: ToPlaceholder[T17] 278 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17)] = 279 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17)](p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17) 280 | 281 | @inline implicit def tuple18ToPlaceholder[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18](implicit 282 | p1: ToPlaceholder[T1], 283 | p2: ToPlaceholder[T2], 284 | p3: ToPlaceholder[T3], 285 | p4: ToPlaceholder[T4], 286 | p5: ToPlaceholder[T5], 287 | p6: ToPlaceholder[T6], 288 | p7: ToPlaceholder[T7], 289 | p8: ToPlaceholder[T8], 290 | p9: ToPlaceholder[T9], 291 | p10: ToPlaceholder[T10], 292 | p11: ToPlaceholder[T11], 293 | p12: ToPlaceholder[T12], 294 | p13: ToPlaceholder[T13], 295 | p14: ToPlaceholder[T14], 296 | p15: ToPlaceholder[T15], 297 | p16: ToPlaceholder[T16], 298 | p17: ToPlaceholder[T17], 299 | p18: ToPlaceholder[T18] 300 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18)] = 301 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18)](p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18) 302 | 303 | @inline implicit def tuple19ToPlaceholder[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19](implicit 304 | p1: ToPlaceholder[T1], 305 | p2: ToPlaceholder[T2], 306 | p3: ToPlaceholder[T3], 307 | p4: ToPlaceholder[T4], 308 | p5: ToPlaceholder[T5], 309 | p6: ToPlaceholder[T6], 310 | p7: ToPlaceholder[T7], 311 | p8: ToPlaceholder[T8], 312 | p9: ToPlaceholder[T9], 313 | p10: ToPlaceholder[T10], 314 | p11: ToPlaceholder[T11], 315 | p12: ToPlaceholder[T12], 316 | p13: ToPlaceholder[T13], 317 | p14: ToPlaceholder[T14], 318 | p15: ToPlaceholder[T15], 319 | p16: ToPlaceholder[T16], 320 | p17: ToPlaceholder[T17], 321 | p18: ToPlaceholder[T18], 322 | p19: ToPlaceholder[T19] 323 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19)] = 324 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19)](p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19) 325 | 326 | @inline implicit def tuple20ToPlaceholder[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20](implicit 327 | p1: ToPlaceholder[T1], 328 | p2: ToPlaceholder[T2], 329 | p3: ToPlaceholder[T3], 330 | p4: ToPlaceholder[T4], 331 | p5: ToPlaceholder[T5], 332 | p6: ToPlaceholder[T6], 333 | p7: ToPlaceholder[T7], 334 | p8: ToPlaceholder[T8], 335 | p9: ToPlaceholder[T9], 336 | p10: ToPlaceholder[T10], 337 | p11: ToPlaceholder[T11], 338 | p12: ToPlaceholder[T12], 339 | p13: ToPlaceholder[T13], 340 | p14: ToPlaceholder[T14], 341 | p15: ToPlaceholder[T15], 342 | p16: ToPlaceholder[T16], 343 | p17: ToPlaceholder[T17], 344 | p18: ToPlaceholder[T18], 345 | p19: ToPlaceholder[T19], 346 | p20: ToPlaceholder[T20] 347 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20)] = 348 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20)](p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19, p20) 349 | 350 | @inline implicit def tuple21ToPlaceholder[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21](implicit 351 | p1: ToPlaceholder[T1], 352 | p2: ToPlaceholder[T2], 353 | p3: ToPlaceholder[T3], 354 | p4: ToPlaceholder[T4], 355 | p5: ToPlaceholder[T5], 356 | p6: ToPlaceholder[T6], 357 | p7: ToPlaceholder[T7], 358 | p8: ToPlaceholder[T8], 359 | p9: ToPlaceholder[T9], 360 | p10: ToPlaceholder[T10], 361 | p11: ToPlaceholder[T11], 362 | p12: ToPlaceholder[T12], 363 | p13: ToPlaceholder[T13], 364 | p14: ToPlaceholder[T14], 365 | p15: ToPlaceholder[T15], 366 | p16: ToPlaceholder[T16], 367 | p17: ToPlaceholder[T17], 368 | p18: ToPlaceholder[T18], 369 | p19: ToPlaceholder[T19], 370 | p20: ToPlaceholder[T20], 371 | p21: ToPlaceholder[T21] 372 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21)] = 373 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21)](p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19, p20, p21) 374 | 375 | @inline implicit def tuple22ToPlaceholder[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22](implicit 376 | p1: ToPlaceholder[T1], 377 | p2: ToPlaceholder[T2], 378 | p3: ToPlaceholder[T3], 379 | p4: ToPlaceholder[T4], 380 | p5: ToPlaceholder[T5], 381 | p6: ToPlaceholder[T6], 382 | p7: ToPlaceholder[T7], 383 | p8: ToPlaceholder[T8], 384 | p9: ToPlaceholder[T9], 385 | p10: ToPlaceholder[T10], 386 | p11: ToPlaceholder[T11], 387 | p12: ToPlaceholder[T12], 388 | p13: ToPlaceholder[T13], 389 | p14: ToPlaceholder[T14], 390 | p15: ToPlaceholder[T15], 391 | p16: ToPlaceholder[T16], 392 | p17: ToPlaceholder[T17], 393 | p18: ToPlaceholder[T18], 394 | p19: ToPlaceholder[T19], 395 | p20: ToPlaceholder[T20], 396 | p21: ToPlaceholder[T21], 397 | p22: ToPlaceholder[T22] 398 | ): ToPlaceholder[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22)] = 399 | new FromTuple[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22)](p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19, p20, p21, p22) 400 | } 401 | -------------------------------------------------------------------------------- /src/main/scala/com/github/tarao/slickjdbc/query/Translator.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | package query 4 | 5 | import slick.jdbc.SQLActionBuilder 6 | 7 | trait Context { 8 | import java.lang.StackTraceElement 9 | 10 | def caller = Thread.currentThread.getStackTrace.reverse.takeWhile { trace => 11 | trace.getClassName != getClass.getName 12 | }.lastOption getOrElse new StackTraceElement("Unknown", "method", null, -1) 13 | } 14 | 15 | trait Translator { 16 | def apply(query: String, context: Context): String 17 | } 18 | 19 | object MarginStripper extends Translator { 20 | def apply(query: String, context: Context) = query.stripMargin 21 | } 22 | 23 | object CallerCommenter extends Translator { 24 | def apply(query: String, context: Context) = 25 | new SQLComment(context.caller).embedTo(query) 26 | } 27 | 28 | case class SQLComment(comment: Any) { 29 | import scala.util.matching.Regex 30 | 31 | def escaped = comment.toString.replace("*/", """*\\/""") 32 | 33 | def embedTo(query: String) = 34 | query.replaceFirst(" ", Regex.quoteReplacement(s" /* ${escaped} */ ")) 35 | } 36 | 37 | object Translator extends Context { 38 | implicit val default: Iterable[Translator] = 39 | Seq(MarginStripper, CallerCommenter) 40 | def translate(query: String)(implicit 41 | translators: Iterable[Translator] 42 | ) = translators.foldLeft(query) { (q, translate) => translate(q, this) } 43 | def translateBuilder(builder: SQLActionBuilder)(implicit 44 | translators: Iterable[Translator] 45 | ): SQLActionBuilder = { 46 | val query = builder.strings.mkString 47 | SQLActionBuilder(Seq(translate(query)(translators)), builder.params) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/resources/application.conf: -------------------------------------------------------------------------------- 1 | h2memtest = { 2 | url = "jdbc:h2:mem:test;DATABASE_TO_UPPER=false;DB_CLOSE_DELAY=-1" 3 | driver = org.h2.Driver 4 | connectionPool = disabled 5 | keepAliveConnection = true 6 | } 7 | -------------------------------------------------------------------------------- /src/test/scala/com/github/tarao/slickjdbc/IntegrationSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | 4 | import eu.timepit.refined.api.Refined 5 | import eu.timepit.refined.auto.autoUnwrap 6 | import eu.timepit.refined.collection.NonEmpty 7 | import eu.timepit.refined.refineV 8 | import helper.{UnitSpec, TestDB, Repository} 9 | import interpolation.{SQLInterpolation, CompoundParameter, TableName} 10 | import getresult.{GetResult, AutoUnwrapOption, TypeBinder} 11 | import slick.jdbc.{SetParameter => SP, PositionedParameters} 12 | import slick.jdbc 13 | 14 | case class URL(url: String) 15 | case class Entry(id: Long, url: URL) 16 | 17 | class MyURL(val url: String) 18 | 19 | /** A sample repository trait */ 20 | trait EntryRepository extends Repository 21 | with SQLInterpolation with CompoundParameter 22 | with GetResult with AutoUnwrapOption { 23 | 24 | implicit object SetMyURL extends SP[MyURL] { 25 | def apply(v: MyURL, pp: PositionedParameters): Unit = { 26 | pp.setString(v.url) 27 | } 28 | } 29 | 30 | implicit val getEntryResult: jdbc.GetResult[Entry] = getResult { Entry( 31 | column("entry_id"), 32 | column("url") 33 | ) } 34 | implicit val getTupleResult: jdbc.GetResult[(Long, String)] = getResult { 35 | ( column[Long]("entry_id"), column[String]("url") ) 36 | } 37 | implicit def urlBinder(implicit 38 | binder: TypeBinder[Option[String]] 39 | ): TypeBinder[Option[URL]] = binder.map(_.map(URL(_))) 40 | 41 | val table = TableName("entry") 42 | 43 | def add1(entry: Entry) = 44 | db.run { sqlu""" 45 | | INSERT INTO ${table} (entry_id, url) 46 | | VALUES (${entry.id}, ${entry.url}) 47 | """ } 48 | 49 | def add2(entry: Entry) = 50 | db.run { sqlu""" 51 | | INSERT INTO ${table} (entry_id, url) 52 | | VALUES ${(entry.id, entry.url)} 53 | """ } 54 | 55 | def add1(entries: Seq[Entry] Refined NonEmpty) = 56 | db.run { sqlu""" 57 | | INSERT INTO ${table} (entry_id, url) 58 | | VALUES $entries 59 | """ } 60 | 61 | def add2(entries: Seq[(Long, URL)] Refined NonEmpty) = 62 | db.run { sqlu""" 63 | | INSERT INTO ${table} (entry_id, url) 64 | | VALUES $entries 65 | """ } 66 | 67 | def add3(entries: Seq[(Long, MyURL)] Refined NonEmpty) = 68 | db.run { sqlu""" 69 | | INSERT INTO ${table} (entry_id, url) 70 | | VALUES $entries 71 | """ } 72 | 73 | def find(entryId: Long): Option[Entry] = 74 | db.run { sql""" 75 | | SELECT * FROM ${table} 76 | | WHERE entry_id = $entryId 77 | """.as[Entry] }.headOption 78 | 79 | def findAsTuple(entryId: Long): Option[(Long, String)] = 80 | db.run { sql""" 81 | | SELECT * FROM ${table} 82 | | WHERE entry_id = $entryId 83 | """.as[(Long, String)] }.headOption 84 | 85 | def find1(entryIds: Seq[Long] Refined NonEmpty) = 86 | db.run { sql""" 87 | | SELECT * FROM ${table} 88 | | WHERE entry_id IN ($entryIds) 89 | | ORDER BY entry_id ASC 90 | """.as[Entry] } 91 | 92 | def findByUrls1(urls: Seq[URL] Refined NonEmpty) = 93 | db.run { sql""" 94 | | SELECT * FROM ${table} 95 | | WHERE url IN ($urls) 96 | | ORDER BY entry_id ASC 97 | """.as[Entry] } 98 | } 99 | 100 | class IntegrationSpec extends UnitSpec with TestDB with EntryRepository { 101 | def freshEntry = { 102 | val id = helper.FreshId() 103 | Entry(id, URL("http://example.com/" + id)) 104 | } 105 | 106 | describe("SELECTing a record") { 107 | it("should succeed") { 108 | val entry = freshEntry 109 | add1(entry) 110 | val result = find(entry.id) 111 | result shouldBe Some(entry) 112 | } 113 | 114 | it("should fail if nothing matched") { 115 | find(helper.FreshId.max) shouldBe empty 116 | } 117 | } 118 | 119 | describe("SELECTing a record as a tuple") { 120 | it("should succeed") { 121 | val entry = freshEntry 122 | add1(entry) 123 | val result = findAsTuple(entry.id) 124 | result shouldBe Some( (entry.id, entry.url.url) ) 125 | } 126 | 127 | it("should fail if nothing matched") { 128 | findAsTuple(helper.FreshId.max) shouldBe empty 129 | } 130 | } 131 | 132 | describe("SELECTing multiple entries") { 133 | it("should succeed") { 134 | val entries1 = Iterator.continually{ freshEntry }.take(10).toSeq 135 | for (e <- entries1) add1(e) 136 | 137 | val entryIds = scala.util.Random.shuffle(entries1.map(_.id)).toSeq 138 | val Right(entryIds1) = refineV[NonEmpty](entryIds) 139 | val result1 = find1(entryIds1) 140 | result1 should be (entries1) 141 | } 142 | 143 | it("should return an empty list if nothing matched") { 144 | val entryIds = Iterator.continually{ helper.FreshId()+0L }.take(10).toSeq 145 | val Right(entryIds1) = 146 | refineV[NonEmpty](scala.util.Random.shuffle(entryIds).toSeq) 147 | val result = find1(entryIds1) 148 | result shouldBe Seq.empty 149 | } 150 | } 151 | 152 | describe("INSERTing multiple entries") { 153 | it("should succeed") { 154 | locally { 155 | val entries = Iterator.continually{ freshEntry }.take(10).toSeq 156 | val Right(entries1) = refineV[NonEmpty](entries) 157 | add1(entries1) 158 | 159 | val Right(entryIds) = refineV[NonEmpty](entries.map(_.id).toSeq) 160 | val result = find1(entryIds) 161 | result should be (entries) 162 | } 163 | 164 | locally { 165 | val entries = Iterator.continually{ freshEntry }.take(10).toSeq 166 | val Right(entries1) = 167 | refineV[NonEmpty](entries.map{ e => (e.id, e.url) }) 168 | add2(entries1) 169 | 170 | val Right(urls) = refineV[NonEmpty](entries.map(_.url).toSeq) 171 | val result = findByUrls1(urls) 172 | result should be (entries) 173 | } 174 | 175 | locally { 176 | val entries = Iterator.continually{ freshEntry }.take(10).toSeq 177 | val Right(entries1) = 178 | refineV[NonEmpty](entries.map{ e => (e.id, new MyURL(e.url.url)) }) 179 | add3(entries1) 180 | 181 | val Right(urls) = refineV[NonEmpty](entries.map(_.url).toSeq) 182 | val result = findByUrls1(urls) 183 | result should be (entries) 184 | } 185 | } 186 | } 187 | } 188 | 189 | /** A sample repository trait */ 190 | trait IdsRepository extends Repository 191 | with SQLInterpolation with CompoundParameter 192 | with GetResult with AutoUnwrapOption { 193 | 194 | def add1(ids: Seq[Tuple1[Long]] Refined NonEmpty) = 195 | db.run { sqlu""" 196 | | INSERT INTO ids (id) 197 | | VALUES ($ids) 198 | """ } 199 | 200 | def addBadly1(ids: Seq[Long] Refined NonEmpty) = 201 | db.run { sqlu""" 202 | | INSERT INTO ids (id) 203 | | VALUES ($ids) 204 | """ } 205 | 206 | def find1(ids: Seq[Long] Refined NonEmpty) = 207 | db.run { sql""" 208 | | SELECT * FROM ids 209 | | WHERE id IN $ids 210 | | ORDER BY id ASC 211 | """.as[Long] } 212 | } 213 | 214 | class SingleTupleSpec extends UnitSpec with TestDB with IdsRepository { 215 | def freshId = helper.FreshId().asInstanceOf[Long] 216 | 217 | describe("SELECTing multiple records INSERTed by single tuples") { 218 | it("should succeed") { 219 | val ids = Iterator.continually{ freshId }.take(10).toSeq 220 | val tuples = ids.map(Tuple1(_)) 221 | val Right(tuples1) = refineV[NonEmpty](tuples) 222 | add1(tuples1) 223 | 224 | val Right(ids1) = 225 | refineV[NonEmpty](scala.util.Random.shuffle(ids).toSeq) 226 | val result = find1(ids1) 227 | result should be (ids) 228 | } 229 | } 230 | 231 | describe("INSERTing single columns by a list") { 232 | it("should fail") { 233 | val ids = Iterator.continually{ freshId }.take(10).toSeq 234 | val Right(ids1) = refineV[NonEmpty](ids) 235 | a [java.sql.SQLException] should be thrownBy addBadly1(ids1) 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/test/scala/com/github/tarao/slickjdbc/getresult/GetResultSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | package getresult 4 | 5 | import scala.reflect.Manifest 6 | import helper.{UnitSpec, TraitSingletonBehavior} 7 | import org.scalamock.scalatest.MockFactory 8 | import java.sql.ResultSet 9 | import slick.jdbc.{GetResult => GR, PositionedResult} 10 | 11 | case class OptionedEntry(id: Option[Long], url: Option[String]) 12 | case class SimpleEntry(id: Long, url: String) 13 | 14 | case class URL(url: String) 15 | object URL { 16 | implicit def getUrl(implicit str: TypeBinder[String]): TypeBinder[URL] = 17 | str.map(URL(_)) 18 | } 19 | case class UrlEntry(id: Long, url: URL) 20 | 21 | trait GetResultBehavior { self: UnitSpec => 22 | def positionedResult(rs: ResultSet) = 23 | new PositionedResult(rs) { def close = {} } 24 | 25 | def mappingResult[R : Manifest](rs: ResultSet, expected: R)(implicit 26 | rconv: GR[R] 27 | ) = { 28 | val result = rconv(positionedResult(rs).restart) 29 | result shouldBe a [R] 30 | result should be (expected) 31 | } 32 | } 33 | 34 | class GetResultSpec extends UnitSpec 35 | with GetResultBehavior with MockFactory { 36 | describe("Explicit usage of GetResult") { 37 | it("should resolve a result by named column") { 38 | implicit val getEntryResult = GetResult { r => OptionedEntry( 39 | r.column("entry_id"), 40 | r.column("url") 41 | ) } 42 | 43 | val id = 1234L 44 | val url = "http://github.com/tarao/" 45 | 46 | val rs = mock[ResultSet] 47 | (rs.getObject(_: String)).expects("entry_id").returning( 48 | java.lang.Long.valueOf(id) 49 | ) 50 | (rs.getString(_: String)).expects("url").returning(url) 51 | 52 | it should behave like mappingResult(rs, OptionedEntry( 53 | Option(id), 54 | Option(url)) 55 | ) 56 | } 57 | 58 | it("should resolve a result by named column with options unwrapped") { 59 | import AutoUnwrapOption._ 60 | 61 | implicit val getEntryResult = GetResult { r => SimpleEntry( 62 | r.column("entry_id"), 63 | r.column("url") 64 | ) } 65 | 66 | val id = 1234L 67 | val url = "http://github.com/tarao/" 68 | 69 | val rs = mock[ResultSet] 70 | (rs.getObject(_: String)).expects("entry_id").returning( 71 | java.lang.Long.valueOf(id) 72 | ) 73 | (rs.getString(_: String)).expects("url").returning(url) 74 | 75 | it should behave like mappingResult(rs, SimpleEntry(id, url)) 76 | } 77 | 78 | it("should resolve a result by named column with a custom type") { 79 | import AutoUnwrapOption._ 80 | 81 | implicit val getEntryResult = GetResult { r => UrlEntry( 82 | r.column("entry_id"), 83 | r.column("url") 84 | ) } 85 | 86 | val id = 1234L 87 | val url = "http://github.com/tarao/" 88 | 89 | val rs = mock[ResultSet] 90 | (rs.getObject(_: String)).expects("entry_id").returning( 91 | java.lang.Long.valueOf(id) 92 | ) 93 | (rs.getString(_: String)).expects("url").returning(url) 94 | 95 | it should behave like mappingResult(rs, UrlEntry(id, URL(url))) 96 | } 97 | 98 | it("should resolve a result by positioned column") { 99 | implicit val getEntryResult = GetResult { r => OptionedEntry( 100 | r.<<, 101 | r.< SimpleEntry( 123 | r.<<, 124 | r.skip.<< 125 | ) } 126 | 127 | val id = 1234L 128 | val url = "http://github.com/tarao/" 129 | 130 | val rs = mock[ResultSet] 131 | (rs.getObject(_: Int)).expects(1).returning( 132 | java.lang.Long.valueOf(id) 133 | ) 134 | (rs.getString(_: Int)).expects(3).returning(url) 135 | 136 | it should behave like mappingResult(rs, SimpleEntry(id, url)) 137 | } 138 | 139 | it("should resolve a result by positioned column with a custom type") { 140 | import AutoUnwrapOption._ 141 | 142 | implicit val getEntryResult = GetResult { r => UrlEntry( 143 | r.skip.<<, 144 | r.<< 145 | ) } 146 | 147 | val id = 1234L 148 | val url = "http://github.com/tarao/" 149 | 150 | val rs = mock[ResultSet] 151 | (rs.getObject(_: Int)).expects(2).returning( 152 | java.lang.Long.valueOf(id) 153 | ) 154 | (rs.getString(_: Int)).expects(3).returning(url) 155 | 156 | it should behave like mappingResult(rs, UrlEntry(id, URL(url))) 157 | } 158 | } 159 | } 160 | 161 | class GetResultTraitSpec extends UnitSpec 162 | with GetResult 163 | with GetResultBehavior with MockFactory { 164 | describe("Implicit usage of GetResult") { 165 | it("should resolve a result by named column") { 166 | implicit val getEntryResult = getResult { OptionedEntry( 167 | column("entry_id"), 168 | column("url") 169 | ) } 170 | 171 | val id = 1234L 172 | val url = "http://github.com/tarao/" 173 | 174 | val rs = mock[ResultSet] 175 | (rs.getObject(_: String)).expects("entry_id").returning( 176 | java.lang.Long.valueOf(id) 177 | ) 178 | (rs.getString(_: String)).expects("url").returning(url) 179 | 180 | it should behave like mappingResult(rs, OptionedEntry( 181 | Option(id), 182 | Option(url)) 183 | ) 184 | } 185 | 186 | it("should resolve a result by named column with options unwrapped") { 187 | import AutoUnwrapOption._ 188 | 189 | implicit val getEntryResult = getResult { SimpleEntry( 190 | column("entry_id"), 191 | column("url") 192 | ) } 193 | 194 | val id = 1234L 195 | val url = "http://github.com/tarao/" 196 | 197 | val rs = mock[ResultSet] 198 | (rs.getObject(_: String)).expects("entry_id").returning( 199 | java.lang.Long.valueOf(id) 200 | ) 201 | (rs.getString(_: String)).expects("url").returning(url) 202 | 203 | it should behave like mappingResult(rs, SimpleEntry(id, url)) 204 | } 205 | 206 | it("should resolve a result by named column with a custom type") { 207 | import AutoUnwrapOption._ 208 | 209 | implicit val getEntryResult = getResult { UrlEntry( 210 | column("entry_id"), 211 | column("url") 212 | ) } 213 | 214 | val id = 1234L 215 | val url = "http://github.com/tarao/" 216 | 217 | val rs = mock[ResultSet] 218 | (rs.getObject(_: String)).expects("entry_id").returning( 219 | java.lang.Long.valueOf(id) 220 | ) 221 | (rs.getString(_: String)).expects("url").returning(url) 222 | 223 | it should behave like mappingResult(rs, UrlEntry(id, URL(url))) 224 | } 225 | 226 | it("should resolve a result by positioned column") { 227 | implicit val getEntryResult = getResult { OptionedEntry( 228 | <<, 229 | <= 0).toSeq 1079 | } 1080 | } 1081 | implicit def reader2Seq(r: Reader): Reader2Seq = new Reader2Seq(r) 1082 | 1083 | class InputStream2Seq(val is: InputStream) { 1084 | def toSeq = { 1085 | val reader = new BufferedReader(new InputStreamReader(is)) 1086 | Iterator.continually{reader.read}.takeWhile(_ >= 0).toSeq 1087 | } 1088 | } 1089 | implicit def inputStream2Seq(is: InputStream): Reader2Seq = 1090 | new Reader2Seq(new InputStreamReader(is)) 1091 | 1092 | describe("TypeBinder[java.io.InputStream]") { 1093 | it("should be able to get an InputStream value") { 1094 | val rs = mock[ResultSet] 1095 | (rs.getBinaryStream(_: Int)).expects(1).returning( 1096 | new ByteArrayInputStream(Array[Byte](1, 2, 3)) 1097 | ) 1098 | (rs.getBinaryStream(_: String)).expects("column1").returning( 1099 | new ByteArrayInputStream(Array[Byte](1, 2, 3)) 1100 | ) 1101 | (rs.getBinaryStream(_: Int)).expects(2).returning( 1102 | new ByteArrayInputStream(Array[Byte](4, 5, 6)) 1103 | ) 1104 | (rs.getBinaryStream(_: String)).expects("column2").returning( 1105 | new ByteArrayInputStream(Array[Byte](4, 5, 6)) 1106 | ) 1107 | 1108 | implicitly[TypeBinder[Option[InputStream]]].apply(rs, 1) 1109 | .get.toSeq should be (Seq(1, 2, 3)) 1110 | implicitly[TypeBinder[Option[InputStream]]].apply(rs, "column1") 1111 | .get.toSeq should be (Seq(1, 2, 3)) 1112 | 1113 | assertTypeError(""" 1114 | implicitly[TypeBinder[InputStream]].apply(rs, 2) 1115 | .toSeq should be (Seq(4, 5, 6)) 1116 | """) 1117 | assertTypeError(""" 1118 | implicitly[TypeBinder[InputStream]].apply(rs, "column2") 1119 | .toSeq should be (Seq(4, 5, 6)) 1120 | """) 1121 | 1122 | import AutoUnwrapOption._ 1123 | 1124 | implicitly[TypeBinder[InputStream]].apply(rs, 2) 1125 | .toSeq should be (Seq(4, 5, 6)) 1126 | implicitly[TypeBinder[InputStream]].apply(rs, "column2") 1127 | .toSeq should be (Seq(4, 5, 6)) 1128 | } 1129 | 1130 | it("should not be be able to get an InputStream from null") { 1131 | val rs = mock[ResultSet] 1132 | (rs.getBinaryStream(_: Int)).expects(0).repeat(2).returning(null) 1133 | (rs.getBinaryStream(_: String)).expects("null").repeat(2).returning(null) 1134 | 1135 | it should behave like column[Option[InputStream]](rs, 0, None) 1136 | it should behave like column[Option[InputStream]](rs, "null", None) 1137 | 1138 | import AutoUnwrapOption._ 1139 | 1140 | it should behave like throwingFromColumn[InputStream](rs, 0) 1141 | it should behave like throwingFromColumn[InputStream](rs, "null") 1142 | } 1143 | } 1144 | 1145 | describe("TypeBinder[java.io.Reader]") { 1146 | it("should be able to get an InputStream value") { 1147 | val rs = mock[ResultSet] 1148 | (rs.getCharacterStream(_: Int)).expects(1).returning( 1149 | new InputStreamReader(new ByteArrayInputStream("foo bar".getBytes)) 1150 | ) 1151 | (rs.getCharacterStream(_: String)).expects("column1").returning( 1152 | new InputStreamReader(new ByteArrayInputStream("foo bar".getBytes)) 1153 | ) 1154 | (rs.getCharacterStream(_: Int)).expects(2).returning( 1155 | new InputStreamReader(new ByteArrayInputStream("foo bar".getBytes)) 1156 | ) 1157 | (rs.getCharacterStream(_: String)).expects("column2").returning( 1158 | new InputStreamReader(new ByteArrayInputStream("foo bar".getBytes)) 1159 | ) 1160 | 1161 | implicitly[TypeBinder[Option[Reader]]].apply(rs, 1) 1162 | .get.toSeq.map(_.asInstanceOf[Char]).mkString should be ("foo bar") 1163 | equal (Seq(1, 2, 3)) 1164 | implicitly[TypeBinder[Option[Reader]]].apply(rs, "column1") 1165 | .get.toSeq.map(_.asInstanceOf[Char]).mkString should be ("foo bar") 1166 | 1167 | assertTypeError(""" 1168 | implicitly[TypeBinder[Reader]].apply(rs, 2) 1169 | .toSeq.map(_.asInstanceOf[Char]).mkString should be ("foo bar") 1170 | """) 1171 | assertTypeError(""" 1172 | implicitly[TypeBinder[Reader]].apply(rs, "column2") 1173 | .toSeq.map(_.asInstanceOf[Char]).mkString should be ("foo bar") 1174 | """) 1175 | 1176 | import AutoUnwrapOption._ 1177 | 1178 | implicitly[TypeBinder[Reader]].apply(rs, 2) 1179 | .toSeq.map(_.asInstanceOf[Char]).mkString should be ("foo bar") 1180 | implicitly[TypeBinder[Reader]].apply(rs, "column2") 1181 | .toSeq.map(_.asInstanceOf[Char]).mkString should be ("foo bar") 1182 | } 1183 | 1184 | it("should not be be able to get an Reader from null") { 1185 | val rs = mock[ResultSet] 1186 | (rs.getCharacterStream(_: Int)).expects(0).repeat(2).returning(null) 1187 | (rs.getCharacterStream(_: String)).expects("null").repeat(2).returning(null) 1188 | 1189 | it should behave like column[Option[Reader]](rs, 0, None) 1190 | it should behave like column[Option[Reader]](rs, "null", None) 1191 | 1192 | import AutoUnwrapOption._ 1193 | 1194 | it should behave like throwingFromColumn[Reader](rs, 0) 1195 | it should behave like throwingFromColumn[Reader](rs, "null") 1196 | } 1197 | } 1198 | } 1199 | 1200 | class AutoUnwrapOptionSpec extends UnitSpec 1201 | with TraitSingletonBehavior { 1202 | describe("object AutoUnwrapOption") { 1203 | it ("shuold inherit the trait") { 1204 | it should behave like exportingTheTraitMethods 1205 | [AutoUnwrapOption](AutoUnwrapOption) 1206 | } 1207 | } 1208 | } 1209 | -------------------------------------------------------------------------------- /src/test/scala/com/github/tarao/slickjdbc/helper/TestDB.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | package helper 4 | 5 | import scala.language.implicitConversions 6 | import scala.concurrent.duration.Duration 7 | import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} 8 | import org.scalatest.funspec.AnyFunSpec 9 | import slick.jdbc.H2Profile.api.Database 10 | 11 | case class Timeout(duration: Duration) 12 | object Timeout { 13 | implicit val forever: Timeout = Timeout(Duration.Inf) 14 | } 15 | 16 | class DBRunner(val db: Database) { 17 | import scala.concurrent.{Future, Await} 18 | import slick.jdbc.H2Profile.api.Database 19 | import slick.dbio.{DBIOAction, NoStream, Effect} 20 | 21 | def run[R](a: DBIOAction[R, NoStream, Nothing])(implicit 22 | timeout: Timeout 23 | ): R = Await.result(db.run(a), timeout.duration) 24 | 25 | def close = db.close 26 | } 27 | 28 | object FreshId { 29 | var id = 0 30 | def apply() = { id = max; id } 31 | def max = { id + 1 } 32 | } 33 | 34 | trait Repository { 35 | def db: DBRunner 36 | } 37 | 38 | trait TestDB extends BeforeAndAfterAll with BeforeAndAfterEach { 39 | self: AnyFunSpec => 40 | 41 | lazy val config = { 42 | import com.typesafe.config.{ConfigFactory, ConfigValueFactory => V} 43 | import slick.jdbc.JdbcDataSource 44 | 45 | // Rewrite database name to thread local one so that writing from 46 | // multiple test threads run parallel won't conflict each other. 47 | val c = ConfigFactory.load.getConfig("h2memtest") 48 | val name = "test" + Thread.currentThread.getId 49 | val url = c.getString("url").replaceFirst("""\btest\b""", name) 50 | c.withValue("url", V.fromAnyRef(url)) 51 | } 52 | 53 | lazy val db = new DBRunner(Database.forConfig("", config)) 54 | 55 | override def beforeAll() = { 56 | import slick.jdbc.H2Profile.api._ 57 | 58 | db.run { sqlu""" 59 | CREATE TABLE IF NOT EXISTS entry ( 60 | entry_id BIGINT NOT NULL PRIMARY KEY, 61 | url VARCHAR(2048) NOT NULL UNIQUE 62 | ) 63 | """ } 64 | 65 | db.run { sqlu""" 66 | CREATE TABLE IF NOT EXISTS ids ( 67 | id BIGINT NOT NULL PRIMARY KEY 68 | ) 69 | """ } 70 | 71 | super.beforeAll() 72 | } 73 | 74 | override def afterAll() = { 75 | db.close 76 | super.afterAll() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/scala/com/github/tarao/slickjdbc/helper/TraitSingletonBehavior.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | package helper 4 | 5 | trait TraitSingletonBehavior { self: UnitSpec => 6 | import scala.reflect.Manifest 7 | import java.lang.Class 8 | 9 | def signatures[T](clazz: Class[T]): Set[String] = 10 | clazz.getDeclaredMethods.map { x => 11 | x.getReturnType.toString + " " + x.getName + 12 | "(" + x.getParameterTypes.mkString(", ") + ")" 13 | }.toSet 14 | 15 | /** 16 | Check a singleton object to export methods in a trait. The object 17 | should implement the trait and have exactly the same methods as 18 | the trait. This ensures that importing the methods by `with 19 | TheTrait` and by `import TheSingleton._` have the the same effect. 20 | */ 21 | def exportingTheTraitMethods[T : Manifest](singleton: Any) = { 22 | singleton shouldBe a [T] 23 | val parent = implicitly[Manifest[T]].runtimeClass.asInstanceOf[Class[T]] 24 | signatures(singleton.getClass) subsetOf (signatures(parent)) shouldBe true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/scala/com/github/tarao/slickjdbc/helper/UnitSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | package helper 4 | 5 | import org.scalatest.Inside 6 | import org.scalatest.Inspectors 7 | import org.scalatest.OptionValues 8 | import org.scalatest.funspec.AnyFunSpec 9 | import org.scalatest.matchers 10 | 11 | abstract class UnitSpec extends AnyFunSpec 12 | with matchers.should.Matchers with OptionValues with Inside with Inspectors 13 | -------------------------------------------------------------------------------- /src/test/scala/com/github/tarao/slickjdbc/interpolation/InterpolationSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | package interpolation 4 | 5 | import helper.{UnitSpec, TraitSingletonBehavior} 6 | import slick.jdbc.TypedParameter 7 | import slick.sql.SqlAction 8 | import slick.dbio.{NoStream, Effect} 9 | 10 | case class EmptyTuple() 11 | case class Single[T](value: T) 12 | case class Double[S, T](left: S, right: T) 13 | case class Triple[S, T, U](left: S, middle: T, right: U) 14 | 15 | class InterpolationSpec extends UnitSpec 16 | with TraitSingletonBehavior { 17 | implicit class EitherOps[A, B](e: Either[A, B]) { // Scala 2.11 compatibility 18 | def toOption: Option[B] = e match { 19 | case Right(x) => Some(x) 20 | case Left(_) => None 21 | } 22 | } 23 | 24 | def canonicalQuery(query: String) = 25 | query.replaceAll("[\r\n\t ]+", " ").trim 26 | 27 | def parsedQuery(sql: SQLActionBuilder): String = 28 | slick.jdbc.SQLInterpolation.parse(sql.strings, sql.params.asInstanceOf[Seq[TypedParameter[Any]]])._1 29 | 30 | def theExactQuery(sql: => SQLActionBuilder)(query: String) = { 31 | parsedQuery(sql) should equal (query) 32 | } 33 | 34 | def anIdenticalQuery(sql: => SQLActionBuilder)(query: String) = { 35 | canonicalQuery(parsedQuery(sql)) should equal (canonicalQuery(query)) 36 | } 37 | 38 | def theExactStatement(sql: => SqlAction[Int, NoStream, Effect]) 39 | (statement: String) = { 40 | val result = sql 41 | result.statements.mkString should equal (statement) 42 | } 43 | 44 | def anIdenticalStatement(sql: => SqlAction[Int, NoStream, Effect]) 45 | (statement: String) = { 46 | val expected = canonicalQuery(statement) 47 | canonicalQuery(sql.statements.mkString) should equal(expected) 48 | } 49 | 50 | describe("object SQLInterpolation") { 51 | it("should inherit the trait") { 52 | it should behave like exportingTheTraitMethods 53 | [SQLInterpolation](SQLInterpolation) 54 | } 55 | } 56 | 57 | describe("SQLInterpolation.sql") { 58 | it("should not be callable without enabling it") { 59 | assertTypeError(""" sql"test" """) 60 | } 61 | 62 | import SQLInterpolation._ 63 | implicit val translators: Iterable[query.Translator] = Seq.empty 64 | 65 | it("should return an action builder with a specified string") { 66 | it should behave like theExactQuery{ sql"test" }("test") 67 | it should behave like anIdenticalQuery { 68 | sql""" 69 | SELECT * FROM entry 70 | LIMIT 1 71 | """ 72 | }("SELECT * FROM entry LIMIT 1") 73 | } 74 | 75 | it("should embed simple values") { 76 | val id = 1234 77 | val url = "http://github.com/tarao/" 78 | 79 | it should behave like anIdenticalQuery { 80 | sql""" 81 | SELECT * FROM entry 82 | WHERE entry_id = $id AND url = $url 83 | """ 84 | }("SELECT * FROM entry WHERE entry_id = ? AND url = ?") 85 | } 86 | 87 | it("should embed a literal string with #${}") { 88 | val tableName = "entry" 89 | 90 | it should behave like anIdenticalQuery { 91 | sql""" 92 | SELECT * FROM #${tableName} 93 | LIMIT 1 94 | """ 95 | }("SELECT * FROM entry LIMIT 1") 96 | } 97 | 98 | it("should embed a literal value") { 99 | val tableName = TableName("entry") 100 | 101 | it should behave like anIdenticalQuery { 102 | sql""" 103 | SELECT * FROM ${tableName} 104 | LIMIT 1 105 | """ 106 | }("SELECT * FROM entry LIMIT 1") 107 | } 108 | 109 | it("should embed a custom literal value") { 110 | val tableName = new Literal { override def toString = "entry" } 111 | 112 | it should behave like anIdenticalQuery { 113 | sql""" 114 | SELECT * FROM ${tableName} 115 | LIMIT 1 116 | """ 117 | }("SELECT * FROM entry LIMIT 1") 118 | } 119 | 120 | it("should not embed an undefined value") { 121 | assertTypeError(""" sql"SELECT * FROM $undefined" """) 122 | } 123 | 124 | it("should not embed a refined non-empty list") { 125 | import eu.timepit.refined.collection.NonEmpty 126 | import eu.timepit.refined.refineV 127 | 128 | val Right(entryIds) = refineV[NonEmpty](Seq(1, 2, 3, 4)) 129 | 130 | assertTypeError(""" 131 | sql"SELECT * FROM entry WHERE entry_id IN ($entryIds)" 132 | """) 133 | } 134 | 135 | it("should embed a refined non-empty list if it is explicitly enabled") { 136 | import eu.timepit.refined.collection.NonEmpty 137 | import eu.timepit.refined.refineV 138 | 139 | val Right(entryIds) = refineV[NonEmpty](Seq(1, 2, 3, 4)) 140 | val Right(entryIds1) = refineV[NonEmpty](Seq(5)) 141 | 142 | import CompoundParameter._ 143 | 144 | it should behave like anIdenticalQuery { 145 | sql"SELECT * FROM entry WHERE entry_id IN ($entryIds)" 146 | }("SELECT * FROM entry WHERE entry_id IN (?, ?, ?, ?)") 147 | 148 | it should behave like anIdenticalQuery { 149 | sql"SELECT * FROM entry WHERE entry_id IN $entryIds" 150 | }("SELECT * FROM entry WHERE entry_id IN (?, ?, ?, ?)") 151 | 152 | it should behave like anIdenticalQuery { 153 | sql"SELECT * FROM entry WHERE entry_id IN (${entryIds1})" 154 | }("SELECT * FROM entry WHERE entry_id IN (?)") 155 | 156 | it should behave like anIdenticalQuery { 157 | sql"SELECT * FROM entry WHERE entry_id IN ${entryIds1}" 158 | }("SELECT * FROM entry WHERE entry_id IN (?)") 159 | } 160 | 161 | it("should embed a refined non-empty set if it is explicitly enabled") { 162 | import eu.timepit.refined.collection.NonEmpty 163 | import eu.timepit.refined.refineV 164 | 165 | val Right(entryIds) = refineV[NonEmpty](Set(1, 2, 3, 4)) 166 | 167 | import CompoundParameter._ 168 | 169 | it should behave like anIdenticalQuery { 170 | sql"SELECT * FROM entry WHERE entry_id IN ($entryIds)" 171 | }("SELECT * FROM entry WHERE entry_id IN (?, ?, ?, ?)") 172 | 173 | it should behave like anIdenticalQuery { 174 | sql"SELECT * FROM entry WHERE entry_id IN $entryIds" 175 | }("SELECT * FROM entry WHERE entry_id IN (?, ?, ?, ?)") 176 | } 177 | 178 | it("should not embed an option non-empty list") { 179 | import eu.timepit.refined.collection.NonEmpty 180 | import eu.timepit.refined.refineV 181 | 182 | import CompoundParameter._ 183 | 184 | val entryIds = refineV[NonEmpty](Seq(1, 2, 3, 4)).toOption 185 | 186 | assertTypeError(""" 187 | sql"SELECT * FROM entry WHERE entry_id IN ($entryIds)" 188 | """) 189 | 190 | it should behave like anIdenticalQuery { 191 | sql"SELECT * FROM entry WHERE entry_id IN (${entryIds.get})" 192 | }("SELECT * FROM entry WHERE entry_id IN (?, ?, ?, ?)") 193 | } 194 | 195 | it("should not embed a refined option non-empty list") { 196 | import CompoundParameter._ 197 | import eu.timepit.refined.collection.NonEmpty 198 | import eu.timepit.refined.refineV 199 | 200 | val entryIds = refineV[NonEmpty](Seq(1, 2, 3, 4)).toOption 201 | 202 | assertTypeError(""" 203 | sql"SELECT * FROM entry WHERE entry_id IN ($entryIds)" 204 | """) 205 | } 206 | 207 | it("should not embed a maybe-empty list") { 208 | val entryIds = Seq(1, 2, 3, 4) 209 | 210 | assertTypeError(""" 211 | sql"SELECT * FROM entry WHERE entry_id IN ($entryIds)" 212 | """) 213 | 214 | import CompoundParameter._ 215 | 216 | assertTypeError(""" 217 | sql"SELECT * FROM entry WHERE entry_id IN ($entryIds)" 218 | """) 219 | } 220 | 221 | it("should embed a tuple") { 222 | val entryIds = (1, 2, 3, 4) 223 | 224 | it should behave like anIdenticalQuery { 225 | sql"SELECT * FROM entry WHERE entry_id IN ($entryIds)" 226 | }("SELECT * FROM entry WHERE entry_id IN (?, ?, ?, ?)") 227 | 228 | it should behave like anIdenticalQuery { 229 | sql"SELECT * FROM entry WHERE entry_id IN $entryIds" 230 | }("SELECT * FROM entry WHERE entry_id IN (?, ?, ?, ?)") 231 | } 232 | 233 | it("should fail to embed a nullary product") { 234 | import CompoundParameter._ 235 | 236 | val empty = EmptyTuple() 237 | a [java.sql.SQLException] should be thrownBy 238 | sql"SELECT * FROM entry WHERE param = $empty" 239 | } 240 | 241 | it("should not embed a product or a signle tuple") { 242 | val tuple1 = Tuple1(5) 243 | val single = Single(5) 244 | val double = Double(1, 2) 245 | val triple = Triple(1, 2, 3) 246 | 247 | assertTypeError(""" 248 | sql"SELECT * FROM entry WHERE entry_id IN ($tuple1)" 249 | """) 250 | 251 | assertTypeError(""" 252 | sql"SELECT * FROM entry WHERE entry_id IN ($single)" 253 | """) 254 | 255 | assertTypeError(""" 256 | sql"SELECT * FROM entry WHERE entry_id IN ($double)" 257 | """) 258 | 259 | assertTypeError(""" 260 | sql"SELECT * FROM entry WHERE entry_id IN ($triple)" 261 | """) 262 | } 263 | 264 | it("should embed a product or a signle tuple if they are explictly enabled") { 265 | val entryIds = (1, 2, 3, 4) 266 | val tuple1 = Tuple1(5) 267 | val single = Single(5) 268 | val double = Double(1, 2) 269 | val triple = Triple(1, 2, 3) 270 | 271 | import CompoundParameter._ 272 | 273 | // check if it doesn't break the normal tuple behavior 274 | it should behave like anIdenticalQuery { 275 | sql"SELECT * FROM entry WHERE entry_id IN ($entryIds)" 276 | }("SELECT * FROM entry WHERE entry_id IN (?, ?, ?, ?)") 277 | 278 | it should behave like anIdenticalQuery { 279 | sql"SELECT * FROM entry WHERE entry_id IN $entryIds" 280 | }("SELECT * FROM entry WHERE entry_id IN (?, ?, ?, ?)") 281 | 282 | it should behave like anIdenticalQuery { 283 | sql"SELECT * FROM entry WHERE entry_id IN ($tuple1)" 284 | }("SELECT * FROM entry WHERE entry_id IN (?)") 285 | it should behave like anIdenticalQuery { 286 | sql"SELECT * FROM entry WHERE entry_id IN $tuple1" 287 | }("SELECT * FROM entry WHERE entry_id IN (?)") 288 | 289 | it should behave like anIdenticalQuery { 290 | sql"SELECT * FROM entry WHERE entry_id IN ($single)" 291 | }("SELECT * FROM entry WHERE entry_id IN (?)") 292 | it should behave like anIdenticalQuery { 293 | sql"SELECT * FROM entry WHERE entry_id IN $single" 294 | }("SELECT * FROM entry WHERE entry_id IN (?)") 295 | 296 | it should behave like anIdenticalQuery { 297 | sql"SELECT * FROM entry WHERE entry_id IN ($double)" 298 | }("SELECT * FROM entry WHERE entry_id IN (?, ?)") 299 | it should behave like anIdenticalQuery { 300 | sql"SELECT * FROM entry WHERE entry_id IN $double" 301 | }("SELECT * FROM entry WHERE entry_id IN (?, ?)") 302 | 303 | it should behave like anIdenticalQuery { 304 | sql"SELECT * FROM entry WHERE entry_id IN ($triple)" 305 | }("SELECT * FROM entry WHERE entry_id IN (?, ?, ?)") 306 | it should behave like anIdenticalQuery { 307 | sql"SELECT * FROM entry WHERE entry_id IN $triple" 308 | }("SELECT * FROM entry WHERE entry_id IN (?, ?, ?)") 309 | } 310 | 311 | it("should not embed a refined non-empty list of product") { 312 | import eu.timepit.refined.collection.NonEmpty 313 | import eu.timepit.refined.refineV 314 | 315 | val Right(params) = refineV[NonEmpty](Seq( 316 | Single("http://example.com/1"), 317 | Single("http://example.com/2"), 318 | Single("http://example.com/3") 319 | )) 320 | 321 | assertTypeError(""" 322 | sql"SELECT * FROM entry WHERE entry_id IN ($params)" 323 | """) 324 | } 325 | 326 | it("should embed a refined non-empty list of product if it is explicitly enabled") { 327 | import eu.timepit.refined.collection.NonEmpty 328 | import eu.timepit.refined.refineV 329 | 330 | // This works as if Single[Int] is an alias of Int 331 | val Right(params) = refineV[NonEmpty](Seq( 332 | Single(1), 333 | Single(2), 334 | Single(3) 335 | )) 336 | 337 | import CompoundParameter._ 338 | 339 | it should behave like anIdenticalQuery { 340 | sql"SELECT * FROM entry WHERE entry_id IN ($params)" 341 | }("SELECT * FROM entry WHERE entry_id IN (?, ?, ?)") 342 | 343 | it should behave like anIdenticalQuery { 344 | sql"SELECT * FROM entry WHERE entry_id IN $params" 345 | }("SELECT * FROM entry WHERE entry_id IN (?, ?, ?)") 346 | } 347 | 348 | it("should not embed a refined option value") { 349 | import eu.timepit.refined.collection.NonEmpty 350 | import eu.timepit.refined.refineV 351 | 352 | val param = Option(3) 353 | val Right(params) = refineV[NonEmpty](Seq(Option(3))) 354 | val tuple = (Option(3), 2, 1) 355 | 356 | assertTypeError(""" 357 | sql"SELECT * FROM entry WHERE param = $param" 358 | """) 359 | 360 | assertTypeError(""" 361 | sql"SELECT * FROM entry WHERE params IN ($params)" 362 | """) 363 | 364 | assertTypeError(""" 365 | sql"SELECT * FROM entry WHERE params IN ($tuple)" 366 | """) 367 | 368 | import CompoundParameter._ 369 | 370 | assertTypeError(""" 371 | sql"SELECT * FROM entry WHERE param = $param" 372 | """) 373 | 374 | assertTypeError(""" 375 | sql"SELECT * FROM entry WHERE params IN ($params)" 376 | """) 377 | 378 | assertTypeError(""" 379 | sql"SELECT * FROM entry WHERE params IN ($tuple)" 380 | """) 381 | } 382 | 383 | it("should not embed a user-defined class value") { 384 | class Foo 385 | val param = new Foo 386 | 387 | import CompoundParameter._ 388 | 389 | assertTypeError(""" 390 | sql"SELECT * FROM entry WHERE param = $param" 391 | """) 392 | } 393 | 394 | it("should embed a user-defined class value with custom SetParameter") { 395 | import slick.jdbc.{SetParameter => SP, PositionedParameters} 396 | class Foo { override def toString = "foo" } 397 | implicit object SetFoo extends SP[Foo] { 398 | def apply(v: Foo, pp: PositionedParameters) = { 399 | pp.setString(v.toString) 400 | } 401 | } 402 | 403 | val param = new Foo 404 | 405 | it should behave like anIdenticalQuery { 406 | sql"SELECT * FROM entry WHERE param = $param" 407 | }("SELECT * FROM entry WHERE param = ?") 408 | } 409 | } 410 | 411 | describe("SQLInterpolation.sql with query translation") { 412 | import SQLInterpolation._ 413 | 414 | it("should provide a default translation") { 415 | sql"test" // just compiles 416 | } 417 | 418 | it("should provide a custom translation") { 419 | implicit val translators: Iterable[query.Translator] = Seq( 420 | new query.Translator { 421 | def apply(q: String, context: query.Context) = q + " translated" 422 | } 423 | ) 424 | it should behave like theExactStatement{ sqlu"test" }("test translated") 425 | } 426 | 427 | it("should provide a custom ordered translation") { 428 | implicit val translators: Iterable[query.Translator] = Seq( 429 | new query.Translator { 430 | def apply(q: String, context: query.Context) = q + " translated" 431 | }, 432 | new query.Translator { 433 | def apply(q: String, context: query.Context) = q + " more" 434 | } 435 | ) 436 | it should behave like theExactStatement{ sqlu"test" }("test translated more") 437 | } 438 | } 439 | 440 | describe("SQLInterpolation.sqlu") { 441 | it("should not be callable without enabling it") { 442 | assertTypeError(""" sqlu"test" """) 443 | } 444 | 445 | import SQLInterpolation._ 446 | implicit val translators: Iterable[query.Translator] = Seq.empty 447 | 448 | it("should return an action with a specified string") { 449 | it should behave like theExactStatement{ sqlu"test" }("test") 450 | it should behave like anIdenticalStatement { 451 | sqlu""" 452 | INSERT INTO entry (entry_id) 453 | VALUES (1) 454 | """ 455 | }("INSERT INTO entry (entry_id) VALUES (1)") 456 | } 457 | 458 | it("should embed simple values") { 459 | val id = 1234 460 | val url = "http://github.com/tarao/" 461 | 462 | it should behave like anIdenticalStatement { 463 | sqlu""" 464 | INSERT INTO entry (entry_id, url) 465 | VALUES ($id, $url) 466 | """ 467 | }("INSERT INTO entry (entry_id, url) VALUES (?, ?)") 468 | } 469 | 470 | it("should embed a literal string with #${}") { 471 | val tableName = "entry" 472 | 473 | it should behave like anIdenticalStatement { 474 | sqlu""" 475 | INSERT INTO #${tableName} (entry_id) 476 | VALUES (1) 477 | """ 478 | }("INSERT INTO entry (entry_id) VALUES (1)") 479 | } 480 | 481 | it("should embed a literal value") { 482 | val tableName = TableName("entry") 483 | 484 | it should behave like anIdenticalStatement { 485 | sqlu""" 486 | INSERT INTO ${tableName} (entry_id) 487 | VALUES (1) 488 | """ 489 | }("INSERT INTO entry (entry_id) VALUES (1)") 490 | } 491 | 492 | it("should embed a custom literal value") { 493 | val tableName = new Literal { override def toString = "entry" } 494 | 495 | it should behave like anIdenticalStatement { 496 | sqlu""" 497 | INSERT INTO ${tableName} (entry_id) 498 | VALUES (1) 499 | """ 500 | }("INSERT INTO entry (entry_id) VALUES (1)") 501 | } 502 | 503 | it("should not embed an undefined value") { 504 | assertTypeError(""" 505 | sqlu"INSERT INTO $undefined (entry_id) VALUES (1)" 506 | """) 507 | } 508 | 509 | it("should not embed a refined non-empty list") { 510 | import eu.timepit.refined.collection.NonEmpty 511 | import eu.timepit.refined.refineV 512 | 513 | val Right(entryIds) = refineV[NonEmpty](Seq(1, 2, 3, 4)) 514 | 515 | assertTypeError(""" 516 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($entryIds)" 517 | """) 518 | } 519 | 520 | it("should embed a refined non-empty list if it is explicitly enabled") { 521 | import eu.timepit.refined.collection.NonEmpty 522 | import eu.timepit.refined.refineV 523 | 524 | import CompoundParameter._ 525 | 526 | val Right(entryIds) = refineV[NonEmpty](Seq(1, 2, 3, 4)) 527 | val Right(entryIds1) = refineV[NonEmpty](Seq(5)) 528 | 529 | it should behave like anIdenticalStatement { 530 | sqlu""" 531 | UPDATE entry 532 | SET flag = 1 533 | WHERE entry_id IN ($entryIds) 534 | """ 535 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?, ?, ?, ?)") 536 | 537 | it should behave like anIdenticalStatement { 538 | sqlu""" 539 | UPDATE entry 540 | SET flag = 1 541 | WHERE entry_id IN $entryIds 542 | """ 543 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?, ?, ?, ?)") 544 | 545 | it should behave like anIdenticalStatement { 546 | sqlu""" 547 | UPDATE entry 548 | SET flag = 1 549 | WHERE entry_id IN (${entryIds1}) 550 | """ 551 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?)") 552 | 553 | it should behave like anIdenticalStatement { 554 | sqlu""" 555 | UPDATE entry 556 | SET flag = 1 557 | WHERE entry_id IN ${entryIds1} 558 | """ 559 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?)") 560 | } 561 | 562 | it("should not embed an option refined non-empty list") { 563 | import eu.timepit.refined.collection.NonEmpty 564 | import eu.timepit.refined.refineV 565 | 566 | import CompoundParameter._ 567 | 568 | val entryIds = refineV[NonEmpty](Seq(1, 2, 3, 4)).toOption 569 | 570 | assertTypeError(""" 571 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($entryIds)" 572 | """) 573 | 574 | it should behave like anIdenticalStatement { 575 | sqlu""" 576 | UPDATE entry 577 | SET flag = 1 578 | WHERE entry_id IN (${entryIds.get}) 579 | """ 580 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?, ?, ?, ?)") 581 | } 582 | 583 | it("should not embed an either refined non-empty list") { 584 | import eu.timepit.refined.collection.NonEmpty 585 | import eu.timepit.refined.refineV 586 | 587 | import CompoundParameter._ 588 | 589 | val entryIds = refineV[NonEmpty](Seq(1, 2, 3, 4)) 590 | 591 | assertTypeError(""" 592 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($entryIds)" 593 | """) 594 | } 595 | 596 | it("should not embed a maybe-empty list") { 597 | val entryIds = Seq(1, 2, 3, 4) 598 | 599 | assertTypeError(""" 600 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($entryIds)" 601 | """) 602 | 603 | import CompoundParameter._ 604 | 605 | assertTypeError(""" 606 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($entryIds)" 607 | """) 608 | } 609 | 610 | it("should embed a tuple") { 611 | val entryIds = (1, 2, 3, 4) 612 | 613 | it should behave like anIdenticalStatement { 614 | sqlu""" 615 | UPDATE entry 616 | SET flag = 1 617 | WHERE entry_id IN ($entryIds) 618 | """ 619 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?, ?, ?, ?)") 620 | 621 | it should behave like anIdenticalStatement { 622 | sqlu""" 623 | UPDATE entry 624 | SET flag = 1 625 | WHERE entry_id IN $entryIds 626 | """ 627 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?, ?, ?, ?)") 628 | } 629 | 630 | it("should fail to embed a nullary product") { 631 | import CompoundParameter._ 632 | 633 | val empty = EmptyTuple() 634 | a [java.sql.SQLException] should be thrownBy 635 | sqlu"UPDATE entry SET flag = 1 WHERE param = $empty" 636 | } 637 | 638 | it("should not embed a product or a signle tuple") { 639 | val tuple1 = Tuple1(5) 640 | val single = Single(5) 641 | val double = Double(1, 2) 642 | val triple = Triple(1, 2, 3) 643 | 644 | assertTypeError(""" 645 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($tuple1)" 646 | """) 647 | 648 | assertTypeError(""" 649 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($single)" 650 | """) 651 | 652 | assertTypeError(""" 653 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($double)" 654 | """) 655 | 656 | assertTypeError(""" 657 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($triple)" 658 | """) 659 | } 660 | 661 | it("should embed a product or a signle tuple if they are explictly enabled") { 662 | val entryIds = (1, 2, 3, 4) 663 | val tuple1 = Tuple1(5) 664 | val single = Single(5) 665 | val double = Double(1, 2) 666 | val triple = Triple(1, 2, 3) 667 | 668 | import CompoundParameter._ 669 | 670 | // check if it doesn't break the normal tuple behavior 671 | it should behave like anIdenticalStatement { 672 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($entryIds)" 673 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?, ?, ?, ?)") 674 | 675 | it should behave like anIdenticalStatement { 676 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN $entryIds" 677 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?, ?, ?, ?)") 678 | 679 | it should behave like anIdenticalStatement { 680 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($tuple1)" 681 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?)") 682 | it should behave like anIdenticalStatement { 683 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN $tuple1" 684 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?)") 685 | 686 | it should behave like anIdenticalStatement { 687 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($single)" 688 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?)") 689 | it should behave like anIdenticalStatement { 690 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN $single" 691 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?)") 692 | 693 | it should behave like anIdenticalStatement { 694 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($double)" 695 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?, ?)") 696 | it should behave like anIdenticalStatement { 697 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN $double" 698 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?, ?)") 699 | 700 | it should behave like anIdenticalStatement { 701 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($triple)" 702 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?, ?, ?)") 703 | it should behave like anIdenticalStatement { 704 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN $triple" 705 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?, ?, ?)") 706 | } 707 | 708 | it("should not embed a refined non-empty list of product") { 709 | import eu.timepit.refined.collection.NonEmpty 710 | import eu.timepit.refined.refineV 711 | 712 | val Right(params1) = refineV[NonEmpty](Seq( 713 | (1, "http://example.com/1"), 714 | (2, "http://example.com/2"), 715 | (3, "http://example.com/3") 716 | )) 717 | 718 | val Right(params2) = refineV[NonEmpty](Seq( 719 | Tuple1("http://example.com/1"), 720 | Tuple1("http://example.com/2"), 721 | Tuple1("http://example.com/3") 722 | )) 723 | 724 | val Right(params3) = refineV[NonEmpty](Seq( 725 | Single("http://example.com/1"), 726 | Single("http://example.com/2"), 727 | Single("http://example.com/3") 728 | )) 729 | 730 | val Right(params4) = refineV[NonEmpty](Seq( 731 | Double(1, "http://example.com/1"), 732 | Double(2, "http://example.com/2"), 733 | Double(3, "http://example.com/3") 734 | )) 735 | 736 | assertTypeError(""" 737 | sqlu"INSERT INTO entry (entry_id, url) VALUES ($params1)" 738 | """) 739 | 740 | assertTypeError(""" 741 | sqlu"INSERT INTO entry (url) VALUES ($params2)" 742 | """) 743 | 744 | assertTypeError(""" 745 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($params3)" 746 | """) 747 | 748 | assertTypeError(""" 749 | sqlu"INSERT INTO entry (entry_id, url) VALUES ($params4)" 750 | """) 751 | } 752 | 753 | it("should embed a refined non-empty list of product if it is explicitly enabled") { 754 | import eu.timepit.refined.collection.NonEmpty 755 | import eu.timepit.refined.refineV 756 | 757 | val Right(params1) = refineV[NonEmpty](Seq( 758 | (1, "http://example.com/1"), 759 | (2, "http://example.com/2"), 760 | (3, "http://example.com/3") 761 | )) 762 | 763 | val Right(params2) = refineV[NonEmpty](Seq( 764 | Tuple1("http://example.com/1"), 765 | Tuple1("http://example.com/2"), 766 | Tuple1("http://example.com/3") 767 | )) 768 | 769 | // This works as if Single[String] is an alias of String 770 | val Right(params3) = refineV[NonEmpty](Seq( 771 | Single("http://example.com/1"), 772 | Single("http://example.com/2"), 773 | Single("http://example.com/3") 774 | )) 775 | 776 | val Right(params4) = refineV[NonEmpty](Seq( 777 | Double(1, "http://example.com/1"), 778 | Double(2, "http://example.com/2"), 779 | Double(3, "http://example.com/3") 780 | )) 781 | 782 | import CompoundParameter._ 783 | 784 | it should behave like anIdenticalStatement { 785 | sqlu"INSERT INTO entry (entry_id, url) VALUES ($params1)" 786 | }("INSERT INTO entry (entry_id, url) VALUES (?, ?), (?, ?), (?, ?)") 787 | 788 | it should behave like anIdenticalStatement { 789 | sqlu"INSERT INTO entry (entry_id, url) VALUES $params1" 790 | }("INSERT INTO entry (entry_id, url) VALUES (?, ?), (?, ?), (?, ?)") 791 | 792 | it should behave like anIdenticalStatement { 793 | sqlu"INSERT INTO entry (url) VALUES ($params2)" 794 | }("INSERT INTO entry (url) VALUES (?), (?), (?)") 795 | 796 | it should behave like anIdenticalStatement { 797 | sqlu"INSERT INTO entry (url) VALUES $params2" 798 | }("INSERT INTO entry (url) VALUES (?), (?), (?)") 799 | 800 | it should behave like anIdenticalStatement { 801 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN ($params3)" 802 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?, ?, ?)") 803 | 804 | it should behave like anIdenticalStatement { 805 | sqlu"UPDATE entry SET flag = 1 WHERE entry_id IN $params3" 806 | }("UPDATE entry SET flag = 1 WHERE entry_id IN (?, ?, ?)") 807 | 808 | it should behave like anIdenticalStatement { 809 | sqlu"INSERT INTO entry (entry_id, url) VALUES ($params4)" 810 | }("INSERT INTO entry (entry_id, url) VALUES (?, ?), (?, ?), (?, ?)") 811 | 812 | it should behave like anIdenticalStatement { 813 | sqlu"INSERT INTO entry (entry_id, url) VALUES $params4" 814 | }("INSERT INTO entry (entry_id, url) VALUES (?, ?), (?, ?), (?, ?)") 815 | } 816 | 817 | it("should not embed a refined option value") { 818 | import eu.timepit.refined.collection.NonEmpty 819 | import eu.timepit.refined.refineV 820 | 821 | val param = Option(3) 822 | val Right(params) = refineV[NonEmpty](Seq(Option(3))) 823 | val tuple = (Option(3), 2, 1) 824 | 825 | assertTypeError(""" 826 | sqlu"UPDATE entry SET flag = 1 WHERE param = $param" 827 | """) 828 | 829 | assertTypeError(""" 830 | sqlu"UPDATE entry SET flag = 1 WHERE params IN ($params)" 831 | """) 832 | 833 | assertTypeError(""" 834 | sqlu"UPDATE entry SET flag = 1 WHERE params IN ($tuple)" 835 | """) 836 | 837 | import CompoundParameter._ 838 | 839 | assertTypeError(""" 840 | sqlu"UPDATE entry SET flag = 1 WHERE param = $param" 841 | """) 842 | 843 | assertTypeError(""" 844 | sqlu"UPDATE entry SET flag = 1 WHERE params IN ($params)" 845 | """) 846 | 847 | assertTypeError(""" 848 | sqlu"UPDATE entry SET flag = 1 WHERE params IN ($tuple)" 849 | """) 850 | } 851 | 852 | it("should not embed a user-defined class value") { 853 | class Foo 854 | val param = new Foo 855 | 856 | import CompoundParameter._ 857 | 858 | assertTypeError(""" 859 | sqlu"UPDATE entry SET flag = 1 WHERE param = $param" 860 | """ 861 | ) 862 | } 863 | 864 | it("should embed a user-defined class value with custom SetParameter") { 865 | import slick.jdbc.{SetParameter => SP, PositionedParameters} 866 | class Foo { override def toString = "foo" } 867 | implicit object SetFoo extends SP[Foo] { 868 | def apply(v: Foo, pp: PositionedParameters) = { 869 | pp.setString(v.toString) 870 | } 871 | } 872 | 873 | val param = new Foo 874 | 875 | it should behave like anIdenticalStatement { 876 | sqlu"UPDATE entry SET flag = 1 WHERE param = $param" 877 | }("UPDATE entry SET flag = 1 WHERE param = ?") 878 | } 879 | } 880 | 881 | describe("SQLInterpolation.sqlu with query translation") { 882 | import SQLInterpolation._ 883 | 884 | it("should provide a default translation") { 885 | sqlu"test" // just compiles 886 | } 887 | 888 | it("should provide a custom translation") { 889 | implicit val translators: Iterable[query.Translator] = Seq( 890 | new query.Translator { 891 | def apply(q: String, context: query.Context) = q + " translated" 892 | } 893 | ) 894 | it should behave like theExactStatement{ sqlu"test" }("test translated") 895 | } 896 | 897 | it("should provide a custom ordered translation") { 898 | implicit val translators: Iterable[query.Translator] = Seq( 899 | new query.Translator { 900 | def apply(q: String, context: query.Context) = q + " translated" 901 | }, 902 | new query.Translator { 903 | def apply(q: String, context: query.Context) = q + " more" 904 | } 905 | ) 906 | it should behave like theExactStatement{ 907 | sqlu"test" 908 | }("test translated more") 909 | } 910 | } 911 | } 912 | 913 | class CompoundParameterSpec extends UnitSpec 914 | with TraitSingletonBehavior { 915 | describe("object ListParameter") { 916 | it("should inherit the trait") { 917 | it should behave like exportingTheTraitMethods 918 | [ListParameter](ListParameter) 919 | } 920 | } 921 | 922 | describe("object ProductParameter") { 923 | it("should inherit the trait") { 924 | it should behave like exportingTheTraitMethods 925 | [ProductParameter](ProductParameter) 926 | } 927 | } 928 | 929 | describe("object CompoundParameter") { 930 | it("should inherit the trait") { 931 | val compoundParameter = new CompoundParameter {} 932 | compoundParameter shouldBe a[ListParameter] 933 | compoundParameter shouldBe a[ProductParameter] 934 | 935 | CompoundParameter shouldBe a[ListParameter] 936 | CompoundParameter shouldBe a[ProductParameter] 937 | } 938 | } 939 | } 940 | -------------------------------------------------------------------------------- /src/test/scala/com/github/tarao/slickjdbc/interpolation/LiteralSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | package interpolation 4 | 5 | import helper.UnitSpec 6 | 7 | class LiteralSpec extends UnitSpec { 8 | describe("SimpleString") { 9 | it("should be a Literal which prints to specified string") { 10 | val simple = new SimpleString("foo") 11 | simple shouldBe a [Literal] 12 | simple.toString should equal ("foo") 13 | } 14 | } 15 | 16 | describe("TableName") { 17 | it("should be a Literal which prints to specified name") { 18 | val table = new TableName("bar") 19 | table shouldBe a [Literal] 20 | table.toString should equal ("bar") 21 | } 22 | 23 | it("should provide a constructor method") { 24 | val table = TableName("baz") 25 | table shouldBe a [Literal] 26 | table.toString should equal ("baz") 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/scala/com/github/tarao/slickjdbc/interpolation/PlaceholderSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | package interpolation 4 | 5 | import helper.UnitSpec 6 | 7 | class PlaceholderSpec extends UnitSpec { 8 | import PlaceholderSpec._ 9 | 10 | def toPlaceholder[T](value: T)(implicit p: ToPlaceholder[T]): Placeholder = 11 | p(value) 12 | 13 | describe("Placeholder of simple types") { 14 | it("should be a Literal") { 15 | val p1 = toPlaceholder(1) 16 | p1 shouldBe a [Literal] 17 | 18 | val p2 = toPlaceholder("foo") 19 | p2 shouldBe a [Literal] 20 | 21 | val p3 = toPlaceholder((1, "foo")) 22 | p3 shouldBe a [Literal] 23 | } 24 | 25 | describe(".toTopLevelString") { 26 | it("should print an empty string for a simple type") { 27 | val p1 = toPlaceholder(1) 28 | p1.toTopLevelString should equal ("") 29 | val p2 = toPlaceholder("foo") 30 | p2.toTopLevelString should equal ("") 31 | val p3 = toPlaceholder(Foo(1, "foo")) 32 | p3.toTopLevelString should equal ("") 33 | val p4 = toPlaceholder(new Bar(1, "foo")) 34 | p4.toTopLevelString should equal ("") 35 | } 36 | 37 | it("should print '?'s x (tuple size - 1)") { 38 | locally { 39 | val p1 = toPlaceholder((1, "foo", 3)) 40 | p1.toTopLevelString should equal ("?, ?, ") 41 | val p2 = toPlaceholder((1, "foo", 3, "bar", 5)) 42 | p2.toTopLevelString should equal ("?, ?, ?, ?, ") 43 | val p3 = toPlaceholder(Tuple1(1)) 44 | p3.toTopLevelString should equal ("") 45 | } 46 | 47 | locally { 48 | val p1 = toPlaceholder(Tuple1(1)) 49 | p1.toTopLevelString should equal ("") 50 | val p2 = toPlaceholder((1, 2)) 51 | p2.toTopLevelString should equal ("?, ") 52 | val p3 = toPlaceholder((1, 2, 3)) 53 | p3.toTopLevelString should equal ("?, ?, ") 54 | val p4 = toPlaceholder((1, 2, 3, 4)) 55 | p4.toTopLevelString should equal ("?, ?, ?, ") 56 | val p5 = toPlaceholder((1, 2, 3, 4, 5)) 57 | p5.toTopLevelString should equal ("?, ?, ?, ?, ") 58 | val p6 = toPlaceholder((1, 2, 3, 4, 5, 6)) 59 | p6.toTopLevelString should equal ("?, ?, ?, ?, ?, ") 60 | val p7 = toPlaceholder((1, 2, 3, 4, 5, 6, 7)) 61 | p7.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ") 62 | val p8 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8)) 63 | p8.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ?, ") 64 | val p9 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9)) 65 | p9.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ?, ?, ") 66 | val p10 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) 67 | p10.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ?, ?, ?, ") 68 | val p11 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)) 69 | p11.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ") 70 | val p12 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)) 71 | p12.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ") 72 | val p13 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)) 73 | p13.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ") 74 | val p14 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)) 75 | p14.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ") 76 | val p15 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)) 77 | p15.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ") 78 | val p16 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)) 79 | p16.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ") 80 | val p17 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)) 81 | p17.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ") 82 | val p18 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18)) 83 | p18.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ") 84 | val p19 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)) 85 | p19.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ") 86 | val p20 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)) 87 | p20.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ") 88 | val p21 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21)) 89 | p21.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ") 90 | val p22 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22)) 91 | p22.toTopLevelString should equal ("?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ") 92 | } 93 | } 94 | 95 | it("should print nested '?'s for nested tuples") { 96 | val p1 = toPlaceholder((("foo", 1), ("bar", 2))) 97 | p1.toTopLevelString should equal ("?, ?), (?, ") 98 | } 99 | 100 | it("should not look into nested products") { 101 | val p1 = toPlaceholder(Baz("simple", 1)) 102 | p1.toTopLevelString should equal ("") 103 | val p2 = toPlaceholder(Some(1)) 104 | p2.toTopLevelString should equal ("") 105 | val p3 = toPlaceholder(Some(Baz("single", 1))) 106 | p3.toTopLevelString should equal ("") 107 | val p4 = toPlaceholder(Pair(Baz("nested", 1), Baz("nested", 2))) 108 | p4.toTopLevelString should equal ("") 109 | } 110 | 111 | it("should be customizable") { 112 | class FromPair[T1, T2](p1: ToPlaceholder[T1], p2: ToPlaceholder[T2]) 113 | extends ToPlaceholder[Pair[T1, T2]] { 114 | def apply(value: Pair[T1, T2]): Placeholder = Placeholder.Nested( 115 | p1(value.a), 116 | p2(value.b) 117 | ) 118 | } 119 | implicit def pairToPlaceholder[T1, T2](implicit 120 | p1: ToPlaceholder[T1], 121 | p2: ToPlaceholder[T2] 122 | ): ToPlaceholder[Pair[T1, T2]] = new FromPair[T1, T2](p1, p2) 123 | 124 | val p1 = toPlaceholder(Pair(Baz("nested", 1), Baz("nested", 2))) 125 | p1.toTopLevelString should equal ("?, ") 126 | val p2 = toPlaceholder(Pair((1, 2), (3, 4))) 127 | p2.toTopLevelString should equal ("?, ?), (?, ") 128 | } 129 | } 130 | 131 | describe(".toString") { 132 | it("should print a single '?' for a simple type") { 133 | val p1 = toPlaceholder(1) 134 | p1.toString should equal ("?") 135 | val p2 = toPlaceholder("foo") 136 | p2.toString should equal ("?") 137 | val p3 = toPlaceholder(Foo(1, "foo")) 138 | p3.toString should equal ("?") 139 | val p4 = toPlaceholder(new Bar(1, "foo")) 140 | p4.toString should equal ("?") 141 | } 142 | 143 | it("should print '?'s x (tuple size) with parenthesis") { 144 | locally { 145 | val p1 = toPlaceholder((1, "foo", 3)) 146 | p1.toString should equal ("(?, ?, ?)") 147 | val p2 = toPlaceholder((1, "foo", 3, "bar", 5)) 148 | p2.toString should equal ("(?, ?, ?, ?, ?)") 149 | val p3 = toPlaceholder(Tuple1(1)) 150 | p3.toString should equal ("(?)") 151 | } 152 | 153 | locally { 154 | val p1 = toPlaceholder(Tuple1(1)) 155 | p1.toString should equal ("(?)") 156 | val p2 = toPlaceholder((1, 2)) 157 | p2.toString should equal ("(?, ?)") 158 | val p3 = toPlaceholder((1, 2, 3)) 159 | p3.toString should equal ("(?, ?, ?)") 160 | val p4 = toPlaceholder((1, 2, 3, 4)) 161 | p4.toString should equal ("(?, ?, ?, ?)") 162 | val p5 = toPlaceholder((1, 2, 3, 4, 5)) 163 | p5.toString should equal ("(?, ?, ?, ?, ?)") 164 | val p6 = toPlaceholder((1, 2, 3, 4, 5, 6)) 165 | p6.toString should equal ("(?, ?, ?, ?, ?, ?)") 166 | val p7 = toPlaceholder((1, 2, 3, 4, 5, 6, 7)) 167 | p7.toString should equal ("(?, ?, ?, ?, ?, ?, ?)") 168 | val p8 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8)) 169 | p8.toString should equal ("(?, ?, ?, ?, ?, ?, ?, ?)") 170 | val p9 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9)) 171 | p9.toString should equal ("(?, ?, ?, ?, ?, ?, ?, ?, ?)") 172 | val p10 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) 173 | p10.toString should equal ("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") 174 | val p11 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)) 175 | p11.toString should equal ("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") 176 | val p12 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)) 177 | p12.toString should equal ("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") 178 | val p13 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)) 179 | p13.toString should equal ("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") 180 | val p14 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14)) 181 | p14.toString should equal ("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") 182 | val p15 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)) 183 | p15.toString should equal ("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") 184 | val p16 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)) 185 | p16.toString should equal ("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") 186 | val p17 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)) 187 | p17.toString should equal ("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") 188 | val p18 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18)) 189 | p18.toString should equal ("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") 190 | val p19 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)) 191 | p19.toString should equal ("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") 192 | val p20 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)) 193 | p20.toString should equal ("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") 194 | val p21 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21)) 195 | p21.toString should equal ("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") 196 | val p22 = toPlaceholder((1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22)) 197 | p22.toString should equal ("(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") 198 | } 199 | } 200 | 201 | it("should print nested '?'s for nested tuples") { 202 | val p1 = toPlaceholder((("foo", 1), ("bar", 2))) 203 | p1.toString should equal ("(?, ?), (?, ?)") 204 | } 205 | 206 | it("should not look into nested products") { 207 | val p1 = toPlaceholder(Baz("simple", 1)) 208 | p1.toString should equal ("?") 209 | val p2 = toPlaceholder(Some(1)) 210 | p2.toString should equal ("?") 211 | val p3 = toPlaceholder(Some(Baz("single", 1))) 212 | p3.toString should equal ("?") 213 | val p4 = toPlaceholder(Pair(Baz("nested", 1), Baz("nested", 2))) 214 | p4.toString should equal ("?") 215 | } 216 | 217 | it("should be customizable") { 218 | class FromPair[T1, T2](p1: ToPlaceholder[T1], p2: ToPlaceholder[T2]) 219 | extends ToPlaceholder[Pair[T1, T2]] { 220 | def apply(value: Pair[T1, T2]): Placeholder = Placeholder.Nested( 221 | p1(value.a), 222 | p2(value.b) 223 | ) 224 | } 225 | implicit def pairToPlaceholder[T1, T2](implicit 226 | p1: ToPlaceholder[T1], 227 | p2: ToPlaceholder[T2] 228 | ): ToPlaceholder[Pair[T1, T2]] = new FromPair[T1, T2](p1, p2) 229 | 230 | val p1 = toPlaceholder(Pair(Baz("nested", 1), Baz("nested", 2))) 231 | p1.toString should equal ("(?, ?)") 232 | val p2 = toPlaceholder(Pair((1, 2), (3, 4))) 233 | p2.toString should equal ("(?, ?), (?, ?)") 234 | } 235 | } 236 | } 237 | 238 | describe("Placeholder of compound parameters") { 239 | import eu.timepit.refined.collection.NonEmpty 240 | import eu.timepit.refined.refineV 241 | import CompoundParameter._ 242 | 243 | it("should be a Literal") { 244 | val p1 = toPlaceholder(Foo(1, "foo")) 245 | p1 shouldBe a [Literal] 246 | 247 | val Right(nel) = refineV[NonEmpty](Seq(1, 2, 3)) 248 | val p2 = toPlaceholder(nel) 249 | p2 shouldBe a [Literal] 250 | } 251 | 252 | it("should not be instantiated by a nullary product") { 253 | a[java.sql.SQLException] should be thrownBy toPlaceholder(None) 254 | } 255 | 256 | describe(".toTopLevelString") { 257 | it("should print '?'s x (list size - 1)") { 258 | val Right(nel1) = refineV[NonEmpty](Seq(1, 2, 3)) 259 | val Right(nel2) = refineV[NonEmpty](Seq(1, 2, 3, 4, 5)) 260 | val Right(nel3) = refineV[NonEmpty](Seq(1)) 261 | 262 | val p1 = toPlaceholder(nel1) 263 | p1.toTopLevelString should equal ("?, ?, ") 264 | val p2 = toPlaceholder(nel2) 265 | p2.toTopLevelString should equal ("?, ?, ?, ?, ") 266 | val p3 = toPlaceholder(nel3) 267 | p3.toTopLevelString should equal ("") 268 | } 269 | 270 | it("should print '?'s x (product arity - 1)") { 271 | val p1 = toPlaceholder(Foo(1, "foo")) 272 | p1.toTopLevelString should equal ("?, ") 273 | } 274 | 275 | it("should print nested '?'s for a nested structure") { 276 | val p1 = toPlaceholder(((1, "foo"), (2, "bar"), (3, "baz"))) 277 | p1.toTopLevelString should equal ("?, ?), (?, ?), (?, ") 278 | 279 | val Right(nel1) = refineV[NonEmpty](Seq(1, 2)) 280 | val Right(nel2) = refineV[NonEmpty](Seq(3)) 281 | val Right(nel3) = refineV[NonEmpty](Seq(4, 5)) 282 | val Right(nel4) = refineV[NonEmpty](Seq(nel1, nel2, nel3)) 283 | val Right(nel5) = refineV[NonEmpty](Seq( 284 | (1, "foo"), 285 | (2, "bar"), 286 | (3, "baz") 287 | )) 288 | 289 | val p2 = toPlaceholder(nel4) 290 | p2.toTopLevelString should equal ("?, ?), (?), (?, ") 291 | val p3 = toPlaceholder(nel5) 292 | p3.toTopLevelString should equal ("?, ?), (?, ?), (?, ") 293 | } 294 | 295 | it("should look into the structure of a nested product") { 296 | val p1 = toPlaceholder(Baz("simple", 1)) 297 | p1.toTopLevelString should equal ("?, ") 298 | val p2 = toPlaceholder(Some(1)) 299 | p2.toTopLevelString should equal ("") 300 | val p3 = toPlaceholder(Some(Baz("single", 1))) 301 | p3.toTopLevelString should equal ("?, ") 302 | val p4 = toPlaceholder(Pair(Baz("nested", 1), Baz("nested", 2))) 303 | p4.toTopLevelString should equal ("?, ?, ?, ") 304 | val p5 = toPlaceholder(Baz("nested", (1, Pair(2, 3)))) 305 | p5.toTopLevelString should equal ("?, ?, ?, ") 306 | 307 | val Right(nel) = refineV[NonEmpty](Seq(1, 2)) 308 | 309 | val p6 = toPlaceholder(Baz("a list in product is not expanded", nel)) 310 | p6.toTopLevelString should equal ("?, ") 311 | } 312 | 313 | it("should preserve simple type conversions") { 314 | locally { 315 | val p1 = toPlaceholder(1) 316 | p1.toTopLevelString should equal ("") 317 | val p2 = toPlaceholder("foo") 318 | p2.toTopLevelString should equal ("") 319 | val p3 = toPlaceholder(new Bar(1, "foo")) 320 | p3.toTopLevelString should equal ("") 321 | } 322 | 323 | locally { 324 | val p1 = toPlaceholder((1, "foo", 3)) 325 | p1.toTopLevelString should equal ("?, ?, ") 326 | val p2 = toPlaceholder((1, "foo", 3, "bar", 5)) 327 | p2.toTopLevelString should equal ("?, ?, ?, ?, ") 328 | val p3 = toPlaceholder(Tuple1(1)) 329 | p3.toTopLevelString should equal ("") 330 | } 331 | 332 | locally { 333 | val p1 = toPlaceholder((("foo", 1), ("bar", 2))) 334 | p1.toTopLevelString should equal ("?, ?), (?, ") 335 | } 336 | } 337 | 338 | it("should be customizable") { 339 | class FromPair[T1, T2] extends ToPlaceholder[Pair[T1, T2]] { 340 | def apply(value: Pair[T1, T2]): Placeholder = Placeholder() 341 | } 342 | implicit def pairToPlaceholder[T1, T2]: ToPlaceholder[Pair[T1, T2]] = 343 | new FromPair[T1, T2] 344 | 345 | val p1 = toPlaceholder(Pair(Baz("nested", 1), Baz("nested", 2))) 346 | p1.toTopLevelString should equal ("") 347 | val p2 = toPlaceholder(Pair((1, 2), (3, 4))) 348 | p2.toTopLevelString should equal ("") 349 | } 350 | } 351 | 352 | describe(".toString") { 353 | it("should print '?'s x (list size) with parenthesis") { 354 | val Right(nel1) = refineV[NonEmpty](Seq(1, 2, 3)) 355 | val Right(nel2) = refineV[NonEmpty](Seq(1, 2, 3, 4, 5)) 356 | val Right(nel3) = refineV[NonEmpty](Seq(1)) 357 | 358 | val p1 = toPlaceholder(nel1) 359 | p1.toString should equal ("(?, ?, ?)") 360 | val p2 = toPlaceholder(nel2) 361 | p2.toString should equal ("(?, ?, ?, ?, ?)") 362 | val p3 = toPlaceholder(nel3) 363 | p3.toString should equal ("(?)") 364 | } 365 | 366 | it("should print '?'s x (product arity - 1)") { 367 | val p1 = toPlaceholder(Foo(1, "foo")) 368 | p1.toString should equal ("(?, ?)") 369 | } 370 | 371 | it("should print nested '?'s for a nested structure") { 372 | val Right(nel1) = refineV[NonEmpty](Seq(1, 2)) 373 | val Right(nel2) = refineV[NonEmpty](Seq(3)) 374 | val Right(nel3) = refineV[NonEmpty](Seq(4, 5)) 375 | val Right(nel4) = refineV[NonEmpty](Seq(nel1, nel2, nel3)) 376 | val Right(nel5) = refineV[NonEmpty](Seq( 377 | (1, "foo"), 378 | (2, "bar"), 379 | (3, "baz") 380 | )) 381 | 382 | val p1 = toPlaceholder(nel4) 383 | p1.toString should equal ("(?, ?), (?), (?, ?)") 384 | val p2 = toPlaceholder(nel5) 385 | p2.toString should equal ("(?, ?), (?, ?), (?, ?)") 386 | val p3 = toPlaceholder(((1, "foo"), (2, "bar"), (3, "baz"))) 387 | p3.toString should equal ("(?, ?), (?, ?), (?, ?)") 388 | } 389 | 390 | it("should look into the structure of a nested product") { 391 | val p1 = toPlaceholder(Baz("simple", 1)) 392 | p1.toString should equal ("(?, ?)") 393 | val p2 = toPlaceholder(Some(1)) 394 | p2.toString should equal ("?") 395 | val p3 = toPlaceholder(Some(Baz("single", 1))) 396 | p3.toString should equal ("(?, ?)") 397 | val p4 = toPlaceholder(Pair(Baz("nested", 1), Baz("nested", 2))) 398 | p4.toString should equal ("(?, ?, ?, ?)") 399 | val p5 = toPlaceholder(Baz("nested", (1, Pair(2, 3)))) 400 | p5.toString should equal ("(?, ?, ?, ?)") 401 | 402 | val Right(nel) = refineV[NonEmpty](Seq(1, 2)) 403 | val p7 = toPlaceholder(Baz("a list in product is not expanded", nel)) 404 | p7.toString should equal ("(?, ?)") 405 | } 406 | 407 | it("should preserve simple type conversions") { 408 | locally { 409 | val p1 = toPlaceholder(1) 410 | p1.toString should equal ("?") 411 | val p2 = toPlaceholder("foo") 412 | p2.toString should equal ("?") 413 | val p3 = toPlaceholder(new Bar(1, "foo")) 414 | p3.toString should equal ("?") 415 | } 416 | 417 | locally { 418 | val p1 = toPlaceholder((1, "foo", 3)) 419 | p1.toString should equal ("(?, ?, ?)") 420 | val p2 = toPlaceholder((1, "foo", 3, "bar", 5)) 421 | p2.toString should equal ("(?, ?, ?, ?, ?)") 422 | val p3 = toPlaceholder(Tuple1(1)) 423 | p3.toString should equal ("(?)") 424 | } 425 | 426 | locally { 427 | val p1 = toPlaceholder((("foo", 1), ("bar", 2))) 428 | p1.toString should equal ("(?, ?), (?, ?)") 429 | } 430 | } 431 | 432 | it("should be customizable") { 433 | class FromPair[T1, T2] extends ToPlaceholder[Pair[T1, T2]] { 434 | def apply(value: Pair[T1, T2]): Placeholder = Placeholder() 435 | } 436 | implicit def pairToPlaceholder[T1, T2]: ToPlaceholder[Pair[T1, T2]] = 437 | new FromPair[T1, T2] 438 | 439 | val p1 = toPlaceholder(Pair(Baz("nested", 1), Baz("nested", 2))) 440 | p1.toString should equal ("?") 441 | val p2 = toPlaceholder(Pair((1, 2), (3, 4))) 442 | p2.toString should equal ("?") 443 | } 444 | } 445 | } 446 | } 447 | object PlaceholderSpec { 448 | case class Foo(a: Int, b: String) 449 | class Bar(a: Int, b: String) 450 | 451 | case class Baz[T](name: String, value: T) 452 | case class Pair[S, T](a: S, b: T) 453 | } 454 | -------------------------------------------------------------------------------- /src/test/scala/com/github/tarao/slickjdbc/query/TranslatorSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.tarao 2 | package slickjdbc 3 | package query 4 | 5 | import helper.UnitSpec 6 | 7 | class ContextSpec extends UnitSpec { 8 | describe("Context.caller") { 9 | it("should return a caller") { 10 | val context = new Context {} 11 | context.caller.toString should fullyMatch regex ( 12 | "com[.]github[.]tarao[.]slickjdbc[.]query[.]ContextSpec.*[(]TranslatorSpec[.]scala:[0-9]+[)]" 13 | ) 14 | } 15 | } 16 | } 17 | 18 | class SQLCommentSpec extends UnitSpec { 19 | describe("SQLComment") { 20 | it("should do nothing for a query without whitespace") { 21 | SQLComment("comment").embedTo("query") should equal ("query") 22 | } 23 | 24 | it("should insert a comment") { 25 | SQLComment("comment").embedTo("some query") should equal ( 26 | "some /* comment */ query" 27 | ) 28 | } 29 | 30 | it("should escape a comment") { 31 | SQLComment("/* comment */").embedTo("some query") should equal ( 32 | """some /* /* comment *\\/ */ query""" 33 | ) 34 | } 35 | 36 | it("should insert any comment that can be a string") { 37 | val obj = new { override def toString = "comment" } 38 | SQLComment(obj).embedTo("some query") should equal ( 39 | """some /* comment */ query""" 40 | ) 41 | } 42 | } 43 | } 44 | 45 | class CallerCommenterSpec extends UnitSpec { 46 | describe("CallerCommenter") { 47 | it("should embed a caller information as a comment") { 48 | implicit val translators : Iterable[Translator] = Seq(CallerCommenter) 49 | val query = Translator.translate("SELECT * FROM entry LIMIT 1") 50 | val caller = "com[.]github[.]tarao[.]slickjdbc[.]query[.]CallerCommenterSpec.*[(]TranslatorSpec[.]scala:[0-9]+[)]" 51 | query should fullyMatch regex ( 52 | s"SELECT /[*] $caller [*]/ [*] FROM entry LIMIT 1" 53 | ) 54 | } 55 | } 56 | } 57 | 58 | class MarginStripperSpec extends UnitSpec { 59 | describe("MarginStripper") { 60 | it("should strip margin by '|'") { 61 | implicit val translators : Iterable[Translator] = Seq(MarginStripper) 62 | val query = Translator.translate(""" 63 | | SELECT * FROM entry 64 | | LIMIT 1""") 65 | query should equal ("\n SELECT * FROM entry\n LIMIT 1") 66 | } 67 | } 68 | } 69 | 70 | class TranslatorSpec extends UnitSpec { 71 | describe("Translator") { 72 | it("should a Context") { 73 | Translator shouldBe a [Context] 74 | } 75 | 76 | it("should pass itself as a context") { 77 | Translator.translate("")(Seq( 78 | new Translator { 79 | def apply(query: String, context: Context) = { 80 | context should equal (Translator) 81 | query 82 | } 83 | } 84 | )) 85 | } 86 | 87 | it("should do nothing for empty translators") { 88 | Translator.translate("foo")(Seq.empty) should equal ("foo") 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "0.2.0-SNAPSHOT" 2 | --------------------------------------------------------------------------------